@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,172 @@
1
+ import { IndexProxy, LazyIndexWrapper } from "../indexes/lazy-index"
2
+ import {
3
+ createSingleRowRefProxy,
4
+ toExpression,
5
+ } from "../query/builder/ref-proxy"
6
+ import { BTreeIndex } from "../indexes/btree-index"
7
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
8
+ import type { BaseIndex, IndexResolver } from "../indexes/base-index"
9
+ import type { ChangeMessage } from "../types"
10
+ import type { IndexOptions } from "../indexes/index-options"
11
+ import type { SingleRowRefProxy } from "../query/builder/ref-proxy"
12
+ import type { CollectionLifecycleManager } from "./lifecycle"
13
+ import type { CollectionStateManager } from "./state"
14
+
15
+ export class CollectionIndexesManager<
16
+ TOutput extends object = Record<string, unknown>,
17
+ TKey extends string | number = string | number,
18
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
19
+ TInput extends object = TOutput,
20
+ > {
21
+ private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
22
+ private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
23
+
24
+ public lazyIndexes = new Map<number, LazyIndexWrapper<TKey>>()
25
+ public resolvedIndexes = new Map<number, BaseIndex<TKey>>()
26
+ public isIndexesResolved = false
27
+ public indexCounter = 0
28
+
29
+ constructor() {}
30
+
31
+ setDeps(deps: {
32
+ state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
33
+ lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
34
+ }) {
35
+ this.state = deps.state
36
+ this.lifecycle = deps.lifecycle
37
+ }
38
+
39
+ /**
40
+ * Creates an index on a collection for faster queries.
41
+ */
42
+ public createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(
43
+ indexCallback: (row: SingleRowRefProxy<TOutput>) => any,
44
+ config: IndexOptions<TResolver> = {}
45
+ ): IndexProxy<TKey> {
46
+ this.lifecycle.validateCollectionUsable(`createIndex`)
47
+
48
+ const indexId = ++this.indexCounter
49
+ const singleRowRefProxy = createSingleRowRefProxy<TOutput>()
50
+ const indexExpression = indexCallback(singleRowRefProxy)
51
+ const expression = toExpression(indexExpression)
52
+
53
+ // Default to BTreeIndex if no type specified
54
+ const resolver = config.indexType ?? (BTreeIndex as unknown as TResolver)
55
+
56
+ // Create lazy wrapper
57
+ const lazyIndex = new LazyIndexWrapper<TKey>(
58
+ indexId,
59
+ expression,
60
+ config.name,
61
+ resolver,
62
+ config.options,
63
+ this.state.entries()
64
+ )
65
+
66
+ this.lazyIndexes.set(indexId, lazyIndex)
67
+
68
+ // For BTreeIndex, resolve immediately and synchronously
69
+ if ((resolver as unknown) === BTreeIndex) {
70
+ try {
71
+ const resolvedIndex = lazyIndex.getResolved()
72
+ this.resolvedIndexes.set(indexId, resolvedIndex)
73
+ } catch (error) {
74
+ console.warn(`Failed to resolve BTreeIndex:`, error)
75
+ }
76
+ } else if (typeof resolver === `function` && resolver.prototype) {
77
+ // Other synchronous constructors - resolve immediately
78
+ try {
79
+ const resolvedIndex = lazyIndex.getResolved()
80
+ this.resolvedIndexes.set(indexId, resolvedIndex)
81
+ } catch {
82
+ // Fallback to async resolution
83
+ this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
84
+ console.warn(`Failed to resolve single index:`, error)
85
+ })
86
+ }
87
+ } else if (this.isIndexesResolved) {
88
+ // Async loader but indexes are already resolved - resolve this one
89
+ this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
90
+ console.warn(`Failed to resolve single index:`, error)
91
+ })
92
+ }
93
+
94
+ return new IndexProxy(indexId, lazyIndex)
95
+ }
96
+
97
+ /**
98
+ * Resolve all lazy indexes (called when collection first syncs)
99
+ */
100
+ public async resolveAllIndexes(): Promise<void> {
101
+ if (this.isIndexesResolved) return
102
+
103
+ const resolutionPromises = Array.from(this.lazyIndexes.entries()).map(
104
+ async ([indexId, lazyIndex]) => {
105
+ const resolvedIndex = await lazyIndex.resolve()
106
+
107
+ // Build index with current data
108
+ resolvedIndex.build(this.state.entries())
109
+
110
+ this.resolvedIndexes.set(indexId, resolvedIndex)
111
+ return { indexId, resolvedIndex }
112
+ }
113
+ )
114
+
115
+ await Promise.all(resolutionPromises)
116
+ this.isIndexesResolved = true
117
+ }
118
+
119
+ /**
120
+ * Resolve a single index immediately
121
+ */
122
+ private async resolveSingleIndex(
123
+ indexId: number,
124
+ lazyIndex: LazyIndexWrapper<TKey>
125
+ ): Promise<BaseIndex<TKey>> {
126
+ const resolvedIndex = await lazyIndex.resolve()
127
+ resolvedIndex.build(this.state.entries())
128
+ this.resolvedIndexes.set(indexId, resolvedIndex)
129
+ return resolvedIndex
130
+ }
131
+
132
+ /**
133
+ * Get resolved indexes for query optimization
134
+ */
135
+ get indexes(): Map<number, BaseIndex<TKey>> {
136
+ return this.resolvedIndexes
137
+ }
138
+
139
+ /**
140
+ * Updates all indexes when the collection changes
141
+ */
142
+ public updateIndexes(changes: Array<ChangeMessage<TOutput, TKey>>): void {
143
+ for (const index of this.resolvedIndexes.values()) {
144
+ for (const change of changes) {
145
+ switch (change.type) {
146
+ case `insert`:
147
+ index.add(change.key, change.value)
148
+ break
149
+ case `update`:
150
+ if (change.previousValue) {
151
+ index.update(change.key, change.previousValue, change.value)
152
+ } else {
153
+ index.add(change.key, change.value)
154
+ }
155
+ break
156
+ case `delete`:
157
+ index.remove(change.key, change.value)
158
+ break
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Clean up the collection by stopping sync and clearing data
166
+ * This can be called manually or automatically by garbage collection
167
+ */
168
+ public cleanup(): void {
169
+ this.lazyIndexes.clear()
170
+ this.resolvedIndexes.clear()
171
+ }
172
+ }
@@ -0,0 +1,289 @@
1
+ import {
2
+ CollectionInErrorStateError,
3
+ InvalidCollectionStatusTransitionError,
4
+ } from "../errors"
5
+ import {
6
+ safeCancelIdleCallback,
7
+ safeRequestIdleCallback,
8
+ } from "../utils/browser-polyfills"
9
+ import type { IdleCallbackDeadline } from "../utils/browser-polyfills"
10
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
11
+ import type { CollectionConfig, CollectionStatus } from "../types"
12
+ import type { CollectionEventsManager } from "./events"
13
+ import type { CollectionIndexesManager } from "./indexes"
14
+ import type { CollectionChangesManager } from "./changes"
15
+ import type { CollectionSyncManager } from "./sync"
16
+ import type { CollectionStateManager } from "./state"
17
+
18
+ export class CollectionLifecycleManager<
19
+ TOutput extends object = Record<string, unknown>,
20
+ TKey extends string | number = string | number,
21
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
22
+ TInput extends object = TOutput,
23
+ > {
24
+ private config: CollectionConfig<TOutput, TKey, TSchema>
25
+ private id: string
26
+ private indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>
27
+ private events!: CollectionEventsManager
28
+ private changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>
29
+ private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
30
+ private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
31
+
32
+ public status: CollectionStatus = `idle`
33
+ public hasBeenReady = false
34
+ public hasReceivedFirstCommit = false
35
+ public onFirstReadyCallbacks: Array<() => void> = []
36
+ public gcTimeoutId: ReturnType<typeof setTimeout> | null = null
37
+ private idleCallbackId: number | null = null
38
+
39
+ /**
40
+ * Creates a new CollectionLifecycleManager instance
41
+ */
42
+ constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {
43
+ this.config = config
44
+ this.id = id
45
+ }
46
+
47
+ setDeps(deps: {
48
+ indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>
49
+ events: CollectionEventsManager
50
+ changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>
51
+ sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
52
+ state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
53
+ }) {
54
+ this.indexes = deps.indexes
55
+ this.events = deps.events
56
+ this.changes = deps.changes
57
+ this.sync = deps.sync
58
+ this.state = deps.state
59
+ }
60
+
61
+ /**
62
+ * Validates state transitions to prevent invalid status changes
63
+ */
64
+ public validateStatusTransition(
65
+ from: CollectionStatus,
66
+ to: CollectionStatus
67
+ ): void {
68
+ if (from === to) {
69
+ // Allow same state transitions
70
+ return
71
+ }
72
+ const validTransitions: Record<
73
+ CollectionStatus,
74
+ Array<CollectionStatus>
75
+ > = {
76
+ idle: [`loading`, `error`, `cleaned-up`],
77
+ loading: [`initialCommit`, `ready`, `error`, `cleaned-up`],
78
+ initialCommit: [`ready`, `error`, `cleaned-up`],
79
+ ready: [`cleaned-up`, `error`],
80
+ error: [`cleaned-up`, `idle`],
81
+ "cleaned-up": [`loading`, `error`],
82
+ }
83
+
84
+ if (!validTransitions[from].includes(to)) {
85
+ throw new InvalidCollectionStatusTransitionError(from, to, this.id)
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Safely update the collection status with validation
91
+ * @private
92
+ */
93
+ public setStatus(newStatus: CollectionStatus): void {
94
+ this.validateStatusTransition(this.status, newStatus)
95
+ const previousStatus = this.status
96
+ this.status = newStatus
97
+
98
+ // Resolve indexes when collection becomes ready
99
+ if (newStatus === `ready` && !this.indexes.isIndexesResolved) {
100
+ // Resolve indexes asynchronously without blocking
101
+ this.indexes.resolveAllIndexes().catch((error) => {
102
+ console.warn(`Failed to resolve indexes:`, error)
103
+ })
104
+ }
105
+
106
+ // Emit event
107
+ this.events.emitStatusChange(newStatus, previousStatus)
108
+ }
109
+
110
+ /**
111
+ * Validates that the collection is in a usable state for data operations
112
+ * @private
113
+ */
114
+ public validateCollectionUsable(operation: string): void {
115
+ switch (this.status) {
116
+ case `error`:
117
+ throw new CollectionInErrorStateError(operation, this.id)
118
+ case `cleaned-up`:
119
+ // Automatically restart the collection when operations are called on cleaned-up collections
120
+ this.sync.startSync()
121
+ break
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Mark the collection as ready for use
127
+ * This is called by sync implementations to explicitly signal that the collection is ready,
128
+ * providing a more intuitive alternative to using commits for readiness signaling
129
+ * @private - Should only be called by sync implementations
130
+ */
131
+ public markReady(): void {
132
+ // Can transition to ready from loading or initialCommit states
133
+ if (this.status === `loading` || this.status === `initialCommit`) {
134
+ this.setStatus(`ready`)
135
+
136
+ // Call any registered first ready callbacks (only on first time becoming ready)
137
+ if (!this.hasBeenReady) {
138
+ this.hasBeenReady = true
139
+
140
+ // Also mark as having received first commit for backwards compatibility
141
+ if (!this.hasReceivedFirstCommit) {
142
+ this.hasReceivedFirstCommit = true
143
+ }
144
+
145
+ const callbacks = [...this.onFirstReadyCallbacks]
146
+ this.onFirstReadyCallbacks = []
147
+ callbacks.forEach((callback) => callback())
148
+ }
149
+ }
150
+
151
+ // Always notify dependents when markReady is called, after status is set
152
+ // This ensures live queries get notified when their dependencies become ready
153
+ if (this.changes.changeSubscriptions.size > 0) {
154
+ this.changes.emitEmptyReadyEvent()
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Start the garbage collection timer
160
+ * Called when the collection becomes inactive (no subscribers)
161
+ */
162
+ public startGCTimer(): void {
163
+ if (this.gcTimeoutId) {
164
+ clearTimeout(this.gcTimeoutId)
165
+ }
166
+
167
+ const gcTime = this.config.gcTime ?? 300000 // 5 minutes default
168
+
169
+ // If gcTime is 0, GC is disabled
170
+ if (gcTime === 0) {
171
+ return
172
+ }
173
+
174
+ this.gcTimeoutId = setTimeout(() => {
175
+ if (this.changes.activeSubscribersCount === 0) {
176
+ // Schedule cleanup during idle time to avoid blocking the UI thread
177
+ this.scheduleIdleCleanup()
178
+ }
179
+ }, gcTime)
180
+ }
181
+
182
+ /**
183
+ * Cancel the garbage collection timer
184
+ * Called when the collection becomes active again
185
+ */
186
+ public cancelGCTimer(): void {
187
+ if (this.gcTimeoutId) {
188
+ clearTimeout(this.gcTimeoutId)
189
+ this.gcTimeoutId = null
190
+ }
191
+ // Also cancel any pending idle cleanup
192
+ if (this.idleCallbackId !== null) {
193
+ safeCancelIdleCallback(this.idleCallbackId)
194
+ this.idleCallbackId = null
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Schedule cleanup to run during browser idle time
200
+ * This prevents blocking the UI thread during cleanup operations
201
+ */
202
+ private scheduleIdleCleanup(): void {
203
+ // Cancel any existing idle callback
204
+ if (this.idleCallbackId !== null) {
205
+ safeCancelIdleCallback(this.idleCallbackId)
206
+ }
207
+
208
+ // Schedule cleanup with a timeout of 1 second
209
+ // This ensures cleanup happens even if the browser is busy
210
+ this.idleCallbackId = safeRequestIdleCallback(
211
+ (deadline) => {
212
+ // Perform cleanup if we still have no subscribers
213
+ if (this.changes.activeSubscribersCount === 0) {
214
+ const cleanupCompleted = this.performCleanup(deadline)
215
+ // Only clear the callback ID if cleanup actually completed
216
+ if (cleanupCompleted) {
217
+ this.idleCallbackId = null
218
+ }
219
+ } else {
220
+ // No need to cleanup, clear the callback ID
221
+ this.idleCallbackId = null
222
+ }
223
+ },
224
+ { timeout: 1000 }
225
+ )
226
+ }
227
+
228
+ /**
229
+ * Perform cleanup operations, optionally in chunks during idle time
230
+ * @returns true if cleanup was completed, false if it was rescheduled
231
+ */
232
+ private performCleanup(deadline?: IdleCallbackDeadline): boolean {
233
+ // If we have a deadline, we can potentially split cleanup into chunks
234
+ // For now, we'll do all cleanup at once but check if we have time
235
+ const hasTime =
236
+ !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout
237
+
238
+ if (hasTime) {
239
+ // Perform all cleanup operations
240
+ this.events.cleanup()
241
+ this.sync.cleanup()
242
+ this.state.cleanup()
243
+ this.changes.cleanup()
244
+ this.indexes.cleanup()
245
+
246
+ if (this.gcTimeoutId) {
247
+ clearTimeout(this.gcTimeoutId)
248
+ this.gcTimeoutId = null
249
+ }
250
+
251
+ this.hasBeenReady = false
252
+ this.onFirstReadyCallbacks = []
253
+
254
+ // Set status to cleaned-up
255
+ this.setStatus(`cleaned-up`)
256
+ return true
257
+ } else {
258
+ // If we don't have time, reschedule for the next idle period
259
+ this.scheduleIdleCleanup()
260
+ return false
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Register a callback to be executed when the collection first becomes ready
266
+ * Useful for preloading collections
267
+ * @param callback Function to call when the collection first becomes ready
268
+ */
269
+ public onFirstReady(callback: () => void): void {
270
+ // If already ready, call immediately
271
+ if (this.hasBeenReady) {
272
+ callback()
273
+ return
274
+ }
275
+
276
+ this.onFirstReadyCallbacks.push(callback)
277
+ }
278
+
279
+ public cleanup(): void {
280
+ // Cancel any pending idle cleanup
281
+ if (this.idleCallbackId !== null) {
282
+ safeCancelIdleCallback(this.idleCallbackId)
283
+ this.idleCallbackId = null
284
+ }
285
+
286
+ // Perform cleanup immediately (used when explicitly called)
287
+ this.performCleanup()
288
+ }
289
+ }