@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,808 @@
1
+ import {
2
+ CollectionRequiresConfigError,
3
+ CollectionRequiresSyncConfigError,
4
+ } from "../errors"
5
+ import { currentStateAsChanges } from "./change-events"
6
+
7
+ import { CollectionStateManager } from "./state"
8
+ import { CollectionChangesManager } from "./changes"
9
+ import { CollectionLifecycleManager } from "./lifecycle.js"
10
+ import { CollectionSyncManager } from "./sync"
11
+ import { CollectionIndexesManager } from "./indexes"
12
+ import { CollectionMutationsManager } from "./mutations"
13
+ import { CollectionEventsManager } from "./events.js"
14
+ import type { CollectionSubscription } from "./subscription"
15
+ import type { AllCollectionEvents, CollectionEventHandler } from "./events.js"
16
+ import type { BaseIndex, IndexResolver } from "../indexes/base-index.js"
17
+ import type { IndexOptions } from "../indexes/index-options.js"
18
+ import type {
19
+ ChangeMessage,
20
+ CollectionConfig,
21
+ CollectionStatus,
22
+ CurrentStateAsChangesOptions,
23
+ Fn,
24
+ InferSchemaInput,
25
+ InferSchemaOutput,
26
+ InsertConfig,
27
+ OperationConfig,
28
+ SubscribeChangesOptions,
29
+ Transaction as TransactionType,
30
+ UtilsRecord,
31
+ WritableDeep,
32
+ } from "../types"
33
+ import type { SingleRowRefProxy } from "../query/builder/ref-proxy"
34
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
35
+ import type { BTreeIndex } from "../indexes/btree-index.js"
36
+ import type { IndexProxy } from "../indexes/lazy-index.js"
37
+
38
+ /**
39
+ * Enhanced Collection interface that includes both data type T and utilities TUtils
40
+ * @template T - The type of items in the collection
41
+ * @template TKey - The type of the key for the collection
42
+ * @template TUtils - The utilities record type
43
+ * @template TInsertInput - The type for insert operations (can be different from T for schemas with defaults)
44
+ */
45
+ export interface Collection<
46
+ T extends object = Record<string, unknown>,
47
+ TKey extends string | number = string | number,
48
+ TUtils extends UtilsRecord = {},
49
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
50
+ TInsertInput extends object = T,
51
+ > extends CollectionImpl<T, TKey, TUtils, TSchema, TInsertInput> {
52
+ readonly utils: TUtils
53
+ }
54
+
55
+ /**
56
+ * Creates a new Collection instance with the given configuration
57
+ *
58
+ * @template T - The schema type if a schema is provided, otherwise the type of items in the collection
59
+ * @template TKey - The type of the key for the collection
60
+ * @template TUtils - The utilities record type
61
+ * @param options - Collection options with optional utilities
62
+ * @returns A new Collection with utilities exposed both at top level and under .utils
63
+ *
64
+ * @example
65
+ * // Pattern 1: With operation handlers (direct collection calls)
66
+ * const todos = createCollection({
67
+ * id: "todos",
68
+ * getKey: (todo) => todo.id,
69
+ * schema,
70
+ * onInsert: async ({ transaction, collection }) => {
71
+ * // Send to API
72
+ * await api.createTodo(transaction.mutations[0].modified)
73
+ * },
74
+ * onUpdate: async ({ transaction, collection }) => {
75
+ * await api.updateTodo(transaction.mutations[0].modified)
76
+ * },
77
+ * onDelete: async ({ transaction, collection }) => {
78
+ * await api.deleteTodo(transaction.mutations[0].key)
79
+ * },
80
+ * sync: { sync: () => {} }
81
+ * })
82
+ *
83
+ * // Direct usage (handlers manage transactions)
84
+ * const tx = todos.insert({ id: "1", text: "Buy milk", completed: false })
85
+ * await tx.isPersisted.promise
86
+ *
87
+ * @example
88
+ * // Pattern 2: Manual transaction management
89
+ * const todos = createCollection({
90
+ * getKey: (todo) => todo.id,
91
+ * schema: todoSchema,
92
+ * sync: { sync: () => {} }
93
+ * })
94
+ *
95
+ * // Explicit transaction usage
96
+ * const tx = createTransaction({
97
+ * mutationFn: async ({ transaction }) => {
98
+ * // Handle all mutations in transaction
99
+ * await api.saveChanges(transaction.mutations)
100
+ * }
101
+ * })
102
+ *
103
+ * tx.mutate(() => {
104
+ * todos.insert({ id: "1", text: "Buy milk" })
105
+ * todos.update("2", draft => { draft.completed = true })
106
+ * })
107
+ *
108
+ * await tx.isPersisted.promise
109
+ *
110
+ * @example
111
+ * // Using schema for type inference (preferred as it also gives you client side validation)
112
+ * const todoSchema = z.object({
113
+ * id: z.string(),
114
+ * title: z.string(),
115
+ * completed: z.boolean()
116
+ * })
117
+ *
118
+ * const todos = createCollection({
119
+ * schema: todoSchema,
120
+ * getKey: (todo) => todo.id,
121
+ * sync: { sync: () => {} }
122
+ * })
123
+ *
124
+ */
125
+
126
+ // Overload for when schema is provided
127
+ export function createCollection<
128
+ T extends StandardSchemaV1,
129
+ TKey extends string | number = string | number,
130
+ TUtils extends UtilsRecord = {},
131
+ >(
132
+ options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
133
+ schema: T
134
+ utils?: TUtils
135
+ }
136
+ ): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>>
137
+
138
+ // Overload for when no schema is provided
139
+ // the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
140
+ export function createCollection<
141
+ T extends object,
142
+ TKey extends string | number = string | number,
143
+ TUtils extends UtilsRecord = {},
144
+ >(
145
+ options: CollectionConfig<T, TKey, never> & {
146
+ schema?: never // prohibit schema if an explicit type is provided
147
+ utils?: TUtils
148
+ }
149
+ ): Collection<T, TKey, TUtils, never, T>
150
+
151
+ // Implementation
152
+ export function createCollection(
153
+ options: CollectionConfig<any, string | number, any> & {
154
+ schema?: StandardSchemaV1
155
+ utils?: UtilsRecord
156
+ }
157
+ ): Collection<any, string | number, UtilsRecord, any, any> {
158
+ const collection = new CollectionImpl<any, string | number, any, any, any>(
159
+ options
160
+ )
161
+
162
+ // Copy utils to both top level and .utils namespace
163
+ if (options.utils) {
164
+ collection.utils = { ...options.utils }
165
+ } else {
166
+ collection.utils = {}
167
+ }
168
+
169
+ return collection
170
+ }
171
+
172
+ export class CollectionImpl<
173
+ TOutput extends object = Record<string, unknown>,
174
+ TKey extends string | number = string | number,
175
+ TUtils extends UtilsRecord = {},
176
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
177
+ TInput extends object = TOutput,
178
+ > {
179
+ public id: string
180
+ public config: CollectionConfig<TOutput, TKey, TSchema>
181
+
182
+ // Utilities namespace
183
+ // This is populated by createCollection
184
+ public utils: Record<string, Fn> = {}
185
+
186
+ // Managers
187
+ private _events: CollectionEventsManager
188
+ private _changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>
189
+ private _lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
190
+ private _sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
191
+ private _indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>
192
+ private _mutations: CollectionMutationsManager<
193
+ TOutput,
194
+ TKey,
195
+ TUtils,
196
+ TSchema,
197
+ TInput
198
+ >
199
+ // The core state of the collection is "public" so that is accessible in tests
200
+ // and for debugging
201
+ public _state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
202
+
203
+ /**
204
+ * Creates a new Collection instance
205
+ *
206
+ * @param config - Configuration object for the collection
207
+ * @throws Error if sync config is missing
208
+ */
209
+ constructor(config: CollectionConfig<TOutput, TKey, TSchema>) {
210
+ // eslint-disable-next-line
211
+ if (!config) {
212
+ throw new CollectionRequiresConfigError()
213
+ }
214
+
215
+ // eslint-disable-next-line
216
+ if (!config.sync) {
217
+ throw new CollectionRequiresSyncConfigError()
218
+ }
219
+
220
+ if (config.id) {
221
+ this.id = config.id
222
+ } else {
223
+ this.id = crypto.randomUUID()
224
+ }
225
+
226
+ // Set default values for optional config properties
227
+ this.config = {
228
+ ...config,
229
+ autoIndex: config.autoIndex ?? `eager`,
230
+ }
231
+
232
+ this._changes = new CollectionChangesManager()
233
+ this._events = new CollectionEventsManager()
234
+ this._indexes = new CollectionIndexesManager()
235
+ this._lifecycle = new CollectionLifecycleManager(config, this.id)
236
+ this._mutations = new CollectionMutationsManager(config, this.id)
237
+ this._state = new CollectionStateManager(config)
238
+ this._sync = new CollectionSyncManager(config, this.id)
239
+
240
+ this._changes.setDeps({
241
+ collection: this, // Required for passing to CollectionSubscription
242
+ lifecycle: this._lifecycle,
243
+ sync: this._sync,
244
+ events: this._events,
245
+ })
246
+ this._events.setDeps({
247
+ collection: this, // Required for adding to emitted events
248
+ })
249
+ this._indexes.setDeps({
250
+ state: this._state,
251
+ lifecycle: this._lifecycle,
252
+ })
253
+ this._lifecycle.setDeps({
254
+ changes: this._changes,
255
+ events: this._events,
256
+ indexes: this._indexes,
257
+ state: this._state,
258
+ sync: this._sync,
259
+ })
260
+ this._mutations.setDeps({
261
+ collection: this, // Required for passing to config.onInsert/onUpdate/onDelete and annotating mutations
262
+ lifecycle: this._lifecycle,
263
+ state: this._state,
264
+ })
265
+ this._state.setDeps({
266
+ collection: this, // Required for filtering events to only include this collection
267
+ lifecycle: this._lifecycle,
268
+ changes: this._changes,
269
+ indexes: this._indexes,
270
+ })
271
+ this._sync.setDeps({
272
+ collection: this, // Required for passing to config.sync callback
273
+ state: this._state,
274
+ lifecycle: this._lifecycle,
275
+ })
276
+
277
+ // Only start sync immediately if explicitly enabled
278
+ if (config.startSync === true) {
279
+ this._sync.startSync()
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Gets the current status of the collection
285
+ */
286
+ public get status(): CollectionStatus {
287
+ return this._lifecycle.status
288
+ }
289
+
290
+ /**
291
+ * Get the number of subscribers to the collection
292
+ */
293
+ public get subscriberCount(): number {
294
+ return this._changes.activeSubscribersCount
295
+ }
296
+
297
+ /**
298
+ * Register a callback to be executed when the collection first becomes ready
299
+ * Useful for preloading collections
300
+ * @param callback Function to call when the collection first becomes ready
301
+ * @example
302
+ * collection.onFirstReady(() => {
303
+ * console.log('Collection is ready for the first time')
304
+ * // Safe to access collection.state now
305
+ * })
306
+ */
307
+ public onFirstReady(callback: () => void): void {
308
+ return this._lifecycle.onFirstReady(callback)
309
+ }
310
+
311
+ /**
312
+ * Check if the collection is ready for use
313
+ * Returns true if the collection has been marked as ready by its sync implementation
314
+ * @returns true if the collection is ready, false otherwise
315
+ * @example
316
+ * if (collection.isReady()) {
317
+ * console.log('Collection is ready, data is available')
318
+ * // Safe to access collection.state
319
+ * } else {
320
+ * console.log('Collection is still loading')
321
+ * }
322
+ */
323
+ public isReady(): boolean {
324
+ return this._lifecycle.status === `ready`
325
+ }
326
+
327
+ /**
328
+ * Start sync immediately - internal method for compiled queries
329
+ * This bypasses lazy loading for special cases like live query results
330
+ */
331
+ public startSyncImmediate(): void {
332
+ this._sync.startSync()
333
+ }
334
+
335
+ /**
336
+ * Preload the collection data by starting sync if not already started
337
+ * Multiple concurrent calls will share the same promise
338
+ */
339
+ public preload(): Promise<void> {
340
+ return this._sync.preload()
341
+ }
342
+
343
+ /**
344
+ * Get the current value for a key (virtual derived state)
345
+ */
346
+ public get(key: TKey): TOutput | undefined {
347
+ return this._state.get(key)
348
+ }
349
+
350
+ /**
351
+ * Check if a key exists in the collection (virtual derived state)
352
+ */
353
+ public has(key: TKey): boolean {
354
+ return this._state.has(key)
355
+ }
356
+
357
+ /**
358
+ * Get the current size of the collection (cached)
359
+ */
360
+ public get size(): number {
361
+ return this._state.size
362
+ }
363
+
364
+ /**
365
+ * Get all keys (virtual derived state)
366
+ */
367
+ public *keys(): IterableIterator<TKey> {
368
+ yield* this._state.keys()
369
+ }
370
+
371
+ /**
372
+ * Get all values (virtual derived state)
373
+ */
374
+ public *values(): IterableIterator<TOutput> {
375
+ yield* this._state.values()
376
+ }
377
+
378
+ /**
379
+ * Get all entries (virtual derived state)
380
+ */
381
+ public *entries(): IterableIterator<[TKey, TOutput]> {
382
+ yield* this._state.entries()
383
+ }
384
+
385
+ /**
386
+ * Get all entries (virtual derived state)
387
+ */
388
+ public *[Symbol.iterator](): IterableIterator<[TKey, TOutput]> {
389
+ yield* this._state[Symbol.iterator]()
390
+ }
391
+
392
+ /**
393
+ * Execute a callback for each entry in the collection
394
+ */
395
+ public forEach(
396
+ callbackfn: (value: TOutput, key: TKey, index: number) => void
397
+ ): void {
398
+ return this._state.forEach(callbackfn)
399
+ }
400
+
401
+ /**
402
+ * Create a new array with the results of calling a function for each entry in the collection
403
+ */
404
+ public map<U>(
405
+ callbackfn: (value: TOutput, key: TKey, index: number) => U
406
+ ): Array<U> {
407
+ return this._state.map(callbackfn)
408
+ }
409
+
410
+ public getKeyFromItem(item: TOutput): TKey {
411
+ return this.config.getKey(item)
412
+ }
413
+
414
+ /**
415
+ * Creates an index on a collection for faster queries.
416
+ * Indexes significantly improve query performance by allowing constant time lookups
417
+ * and logarithmic time range queries instead of full scans.
418
+ *
419
+ * @template TResolver - The type of the index resolver (constructor or async loader)
420
+ * @param indexCallback - Function that extracts the indexed value from each item
421
+ * @param config - Configuration including index type and type-specific options
422
+ * @returns An index proxy that provides access to the index when ready
423
+ *
424
+ * @example
425
+ * // Create a default B+ tree index
426
+ * const ageIndex = collection.createIndex((row) => row.age)
427
+ *
428
+ * // Create a ordered index with custom options
429
+ * const ageIndex = collection.createIndex((row) => row.age, {
430
+ * indexType: BTreeIndex,
431
+ * options: { compareFn: customComparator },
432
+ * name: 'age_btree'
433
+ * })
434
+ *
435
+ * // Create an async-loaded index
436
+ * const textIndex = collection.createIndex((row) => row.content, {
437
+ * indexType: async () => {
438
+ * const { FullTextIndex } = await import('./indexes/fulltext.js')
439
+ * return FullTextIndex
440
+ * },
441
+ * options: { language: 'en' }
442
+ * })
443
+ */
444
+ public createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(
445
+ indexCallback: (row: SingleRowRefProxy<TOutput>) => any,
446
+ config: IndexOptions<TResolver> = {}
447
+ ): IndexProxy<TKey> {
448
+ return this._indexes.createIndex(indexCallback, config)
449
+ }
450
+
451
+ /**
452
+ * Get resolved indexes for query optimization
453
+ */
454
+ get indexes(): Map<number, BaseIndex<TKey>> {
455
+ return this._indexes.indexes
456
+ }
457
+
458
+ /**
459
+ * Validates the data against the schema
460
+ */
461
+ public validateData(
462
+ data: unknown,
463
+ type: `insert` | `update`,
464
+ key?: TKey
465
+ ): TOutput | never {
466
+ return this._mutations.validateData(data, type, key)
467
+ }
468
+
469
+ /**
470
+ * Inserts one or more items into the collection
471
+ * @param items - Single item or array of items to insert
472
+ * @param config - Optional configuration including metadata
473
+ * @returns A Transaction object representing the insert operation(s)
474
+ * @throws {SchemaValidationError} If the data fails schema validation
475
+ * @example
476
+ * // Insert a single todo (requires onInsert handler)
477
+ * const tx = collection.insert({ id: "1", text: "Buy milk", completed: false })
478
+ * await tx.isPersisted.promise
479
+ *
480
+ * @example
481
+ * // Insert multiple todos at once
482
+ * const tx = collection.insert([
483
+ * { id: "1", text: "Buy milk", completed: false },
484
+ * { id: "2", text: "Walk dog", completed: true }
485
+ * ])
486
+ * await tx.isPersisted.promise
487
+ *
488
+ * @example
489
+ * // Insert with metadata
490
+ * const tx = collection.insert({ id: "1", text: "Buy groceries" },
491
+ * { metadata: { source: "mobile-app" } }
492
+ * )
493
+ * await tx.isPersisted.promise
494
+ *
495
+ * @example
496
+ * // Handle errors
497
+ * try {
498
+ * const tx = collection.insert({ id: "1", text: "New item" })
499
+ * await tx.isPersisted.promise
500
+ * console.log('Insert successful')
501
+ * } catch (error) {
502
+ * console.log('Insert failed:', error)
503
+ * }
504
+ */
505
+ insert = (data: TInput | Array<TInput>, config?: InsertConfig) => {
506
+ return this._mutations.insert(data, config)
507
+ }
508
+
509
+ /**
510
+ * Updates one or more items in the collection using a callback function
511
+ * @param keys - Single key or array of keys to update
512
+ * @param configOrCallback - Either update configuration or update callback
513
+ * @param maybeCallback - Update callback if config was provided
514
+ * @returns A Transaction object representing the update operation(s)
515
+ * @throws {SchemaValidationError} If the updated data fails schema validation
516
+ * @example
517
+ * // Update single item by key
518
+ * const tx = collection.update("todo-1", (draft) => {
519
+ * draft.completed = true
520
+ * })
521
+ * await tx.isPersisted.promise
522
+ *
523
+ * @example
524
+ * // Update multiple items
525
+ * const tx = collection.update(["todo-1", "todo-2"], (drafts) => {
526
+ * drafts.forEach(draft => { draft.completed = true })
527
+ * })
528
+ * await tx.isPersisted.promise
529
+ *
530
+ * @example
531
+ * // Update with metadata
532
+ * const tx = collection.update("todo-1",
533
+ * { metadata: { reason: "user update" } },
534
+ * (draft) => { draft.text = "Updated text" }
535
+ * )
536
+ * await tx.isPersisted.promise
537
+ *
538
+ * @example
539
+ * // Handle errors
540
+ * try {
541
+ * const tx = collection.update("item-1", draft => { draft.value = "new" })
542
+ * await tx.isPersisted.promise
543
+ * console.log('Update successful')
544
+ * } catch (error) {
545
+ * console.log('Update failed:', error)
546
+ * }
547
+ */
548
+
549
+ // Overload 1: Update multiple items with a callback
550
+ update(
551
+ key: Array<TKey | unknown>,
552
+ callback: (drafts: Array<WritableDeep<TInput>>) => void
553
+ ): TransactionType
554
+
555
+ // Overload 2: Update multiple items with config and a callback
556
+ update(
557
+ keys: Array<TKey | unknown>,
558
+ config: OperationConfig,
559
+ callback: (drafts: Array<WritableDeep<TInput>>) => void
560
+ ): TransactionType
561
+
562
+ // Overload 3: Update a single item with a callback
563
+ update(
564
+ id: TKey | unknown,
565
+ callback: (draft: WritableDeep<TInput>) => void
566
+ ): TransactionType
567
+
568
+ // Overload 4: Update a single item with config and a callback
569
+ update(
570
+ id: TKey | unknown,
571
+ config: OperationConfig,
572
+ callback: (draft: WritableDeep<TInput>) => void
573
+ ): TransactionType
574
+
575
+ update(
576
+ keys: (TKey | unknown) | Array<TKey | unknown>,
577
+ configOrCallback:
578
+ | ((draft: WritableDeep<TInput>) => void)
579
+ | ((drafts: Array<WritableDeep<TInput>>) => void)
580
+ | OperationConfig,
581
+ maybeCallback?:
582
+ | ((draft: WritableDeep<TInput>) => void)
583
+ | ((drafts: Array<WritableDeep<TInput>>) => void)
584
+ ) {
585
+ return this._mutations.update(keys, configOrCallback, maybeCallback)
586
+ }
587
+
588
+ /**
589
+ * Deletes one or more items from the collection
590
+ * @param keys - Single key or array of keys to delete
591
+ * @param config - Optional configuration including metadata
592
+ * @returns A Transaction object representing the delete operation(s)
593
+ * @example
594
+ * // Delete a single item
595
+ * const tx = collection.delete("todo-1")
596
+ * await tx.isPersisted.promise
597
+ *
598
+ * @example
599
+ * // Delete multiple items
600
+ * const tx = collection.delete(["todo-1", "todo-2"])
601
+ * await tx.isPersisted.promise
602
+ *
603
+ * @example
604
+ * // Delete with metadata
605
+ * const tx = collection.delete("todo-1", { metadata: { reason: "completed" } })
606
+ * await tx.isPersisted.promise
607
+ *
608
+ * @example
609
+ * // Handle errors
610
+ * try {
611
+ * const tx = collection.delete("item-1")
612
+ * await tx.isPersisted.promise
613
+ * console.log('Delete successful')
614
+ * } catch (error) {
615
+ * console.log('Delete failed:', error)
616
+ * }
617
+ */
618
+ delete = (
619
+ keys: Array<TKey> | TKey,
620
+ config?: OperationConfig
621
+ ): TransactionType<any> => {
622
+ return this._mutations.delete(keys, config)
623
+ }
624
+
625
+ /**
626
+ * Gets the current state of the collection as a Map
627
+ * @returns Map containing all items in the collection, with keys as identifiers
628
+ * @example
629
+ * const itemsMap = collection.state
630
+ * console.log(`Collection has ${itemsMap.size} items`)
631
+ *
632
+ * for (const [key, item] of itemsMap) {
633
+ * console.log(`${key}: ${item.title}`)
634
+ * }
635
+ *
636
+ * // Check if specific item exists
637
+ * if (itemsMap.has("todo-1")) {
638
+ * console.log("Todo 1 exists:", itemsMap.get("todo-1"))
639
+ * }
640
+ */
641
+ get state() {
642
+ const result = new Map<TKey, TOutput>()
643
+ for (const [key, value] of this.entries()) {
644
+ result.set(key, value)
645
+ }
646
+ return result
647
+ }
648
+
649
+ /**
650
+ * Gets the current state of the collection as a Map, but only resolves when data is available
651
+ * Waits for the first sync commit to complete before resolving
652
+ *
653
+ * @returns Promise that resolves to a Map containing all items in the collection
654
+ */
655
+ stateWhenReady(): Promise<Map<TKey, TOutput>> {
656
+ // If we already have data or collection is ready, resolve immediately
657
+ if (this.size > 0 || this.isReady()) {
658
+ return Promise.resolve(this.state)
659
+ }
660
+
661
+ // Use preload to ensure the collection starts loading, then return the state
662
+ return this.preload().then(() => this.state)
663
+ }
664
+
665
+ /**
666
+ * Gets the current state of the collection as an Array
667
+ *
668
+ * @returns An Array containing all items in the collection
669
+ */
670
+ get toArray() {
671
+ return Array.from(this.values())
672
+ }
673
+
674
+ /**
675
+ * Gets the current state of the collection as an Array, but only resolves when data is available
676
+ * Waits for the first sync commit to complete before resolving
677
+ *
678
+ * @returns Promise that resolves to an Array containing all items in the collection
679
+ */
680
+ toArrayWhenReady(): Promise<Array<TOutput>> {
681
+ // If we already have data or collection is ready, resolve immediately
682
+ if (this.size > 0 || this.isReady()) {
683
+ return Promise.resolve(this.toArray)
684
+ }
685
+
686
+ // Use preload to ensure the collection starts loading, then return the array
687
+ return this.preload().then(() => this.toArray)
688
+ }
689
+
690
+ /**
691
+ * Returns the current state of the collection as an array of changes
692
+ * @param options - Options including optional where filter
693
+ * @returns An array of changes
694
+ * @example
695
+ * // Get all items as changes
696
+ * const allChanges = collection.currentStateAsChanges()
697
+ *
698
+ * // Get only items matching a condition
699
+ * const activeChanges = collection.currentStateAsChanges({
700
+ * where: (row) => row.status === 'active'
701
+ * })
702
+ *
703
+ * // Get only items using a pre-compiled expression
704
+ * const activeChanges = collection.currentStateAsChanges({
705
+ * whereExpression: eq(row.status, 'active')
706
+ * })
707
+ */
708
+ public currentStateAsChanges(
709
+ options: CurrentStateAsChangesOptions = {}
710
+ ): Array<ChangeMessage<TOutput>> | void {
711
+ return currentStateAsChanges(this, options)
712
+ }
713
+
714
+ /**
715
+ * Subscribe to changes in the collection
716
+ * @param callback - Function called when items change
717
+ * @param options - Subscription options including includeInitialState and where filter
718
+ * @returns Unsubscribe function - Call this to stop listening for changes
719
+ * @example
720
+ * // Basic subscription
721
+ * const subscription = collection.subscribeChanges((changes) => {
722
+ * changes.forEach(change => {
723
+ * console.log(`${change.type}: ${change.key}`, change.value)
724
+ * })
725
+ * })
726
+ *
727
+ * // Later: subscription.unsubscribe()
728
+ *
729
+ * @example
730
+ * // Include current state immediately
731
+ * const subscription = collection.subscribeChanges((changes) => {
732
+ * updateUI(changes)
733
+ * }, { includeInitialState: true })
734
+ *
735
+ * @example
736
+ * // Subscribe only to changes matching a condition
737
+ * const subscription = collection.subscribeChanges((changes) => {
738
+ * updateUI(changes)
739
+ * }, {
740
+ * includeInitialState: true,
741
+ * where: (row) => row.status === 'active'
742
+ * })
743
+ *
744
+ * @example
745
+ * // Subscribe using a pre-compiled expression
746
+ * const subscription = collection.subscribeChanges((changes) => {
747
+ * updateUI(changes)
748
+ * }, {
749
+ * includeInitialState: true,
750
+ * whereExpression: eq(row.status, 'active')
751
+ * })
752
+ */
753
+ public subscribeChanges(
754
+ callback: (changes: Array<ChangeMessage<TOutput>>) => void,
755
+ options: SubscribeChangesOptions = {}
756
+ ): CollectionSubscription {
757
+ return this._changes.subscribeChanges(callback, options)
758
+ }
759
+
760
+ /**
761
+ * Subscribe to a collection event
762
+ */
763
+ public on<T extends keyof AllCollectionEvents>(
764
+ event: T,
765
+ callback: CollectionEventHandler<T>
766
+ ) {
767
+ return this._events.on(event, callback)
768
+ }
769
+
770
+ /**
771
+ * Subscribe to a collection event once
772
+ */
773
+ public once<T extends keyof AllCollectionEvents>(
774
+ event: T,
775
+ callback: CollectionEventHandler<T>
776
+ ) {
777
+ return this._events.once(event, callback)
778
+ }
779
+
780
+ /**
781
+ * Unsubscribe from a collection event
782
+ */
783
+ public off<T extends keyof AllCollectionEvents>(
784
+ event: T,
785
+ callback: CollectionEventHandler<T>
786
+ ) {
787
+ this._events.off(event, callback)
788
+ }
789
+
790
+ /**
791
+ * Wait for a collection event
792
+ */
793
+ public waitFor<T extends keyof AllCollectionEvents>(
794
+ event: T,
795
+ timeout?: number
796
+ ) {
797
+ return this._events.waitFor(event, timeout)
798
+ }
799
+
800
+ /**
801
+ * Clean up the collection by stopping sync and clearing data
802
+ * This can be called manually or automatically by garbage collection
803
+ */
804
+ public async cleanup(): Promise<void> {
805
+ this._lifecycle.cleanup()
806
+ return Promise.resolve()
807
+ }
808
+ }