@tanstack/db 0.3.1 → 0.4.0

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 (162) 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 +90 -0
  8. package/dist/cjs/collection/events.cjs.map +1 -0
  9. package/dist/cjs/collection/events.d.cts +53 -0
  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} +56 -172
  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 +150 -0
  17. package/dist/cjs/collection/lifecycle.cjs.map +1 -0
  18. package/dist/cjs/collection/lifecycle.d.cts +70 -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/change-events.d.cts → esm/collection/change-events.d.ts} +6 -6
  66. package/dist/esm/{change-events.js → collection/change-events.js} +13 -42
  67. package/dist/esm/collection/change-events.js.map +1 -0
  68. package/dist/esm/collection/changes.d.ts +53 -0
  69. package/dist/esm/collection/changes.js +108 -0
  70. package/dist/esm/collection/changes.js.map +1 -0
  71. package/dist/esm/collection/events.d.ts +53 -0
  72. package/dist/esm/collection/events.js +90 -0
  73. package/dist/esm/collection/events.js.map +1 -0
  74. package/dist/{cjs/collection.d.cts → esm/collection/index.d.ts} +56 -172
  75. package/dist/esm/collection/index.js +417 -0
  76. package/dist/esm/collection/index.js.map +1 -0
  77. package/dist/esm/collection/indexes.d.ts +47 -0
  78. package/dist/esm/collection/indexes.js +124 -0
  79. package/dist/esm/collection/indexes.js.map +1 -0
  80. package/dist/esm/collection/lifecycle.d.ts +70 -0
  81. package/dist/esm/collection/lifecycle.js +150 -0
  82. package/dist/esm/collection/lifecycle.js.map +1 -0
  83. package/dist/esm/collection/mutations.d.ts +33 -0
  84. package/dist/esm/collection/mutations.js +315 -0
  85. package/dist/esm/collection/mutations.js.map +1 -0
  86. package/dist/esm/collection/state.d.ts +122 -0
  87. package/dist/esm/collection/state.js +597 -0
  88. package/dist/esm/collection/state.js.map +1 -0
  89. package/dist/esm/collection/subscription.d.ts +57 -0
  90. package/dist/esm/collection/subscription.js +160 -0
  91. package/dist/esm/collection/subscription.js.map +1 -0
  92. package/dist/esm/collection/sync.d.ts +34 -0
  93. package/dist/esm/collection/sync.js +154 -0
  94. package/dist/esm/collection/sync.js.map +1 -0
  95. package/dist/esm/index.d.ts +2 -2
  96. package/dist/esm/index.js +1 -1
  97. package/dist/esm/indexes/auto-index.d.ts +1 -1
  98. package/dist/esm/indexes/auto-index.js.map +1 -1
  99. package/dist/esm/indexes/base-index.d.ts +2 -2
  100. package/dist/esm/indexes/base-index.js.map +1 -1
  101. package/dist/esm/indexes/btree-index.d.ts +1 -1
  102. package/dist/esm/indexes/btree-index.js +2 -2
  103. package/dist/esm/indexes/btree-index.js.map +1 -1
  104. package/dist/esm/proxy.js +1 -1
  105. package/dist/esm/query/builder/index.js +1 -1
  106. package/dist/esm/query/builder/index.js.map +1 -1
  107. package/dist/esm/query/builder/types.d.ts +1 -1
  108. package/dist/esm/query/compiler/index.d.ts +3 -2
  109. package/dist/esm/query/compiler/index.js +5 -2
  110. package/dist/esm/query/compiler/index.js.map +1 -1
  111. package/dist/esm/query/compiler/joins.d.ts +3 -2
  112. package/dist/esm/query/compiler/joins.js +22 -24
  113. package/dist/esm/query/compiler/joins.js.map +1 -1
  114. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  115. package/dist/esm/query/compiler/order-by.js.map +1 -1
  116. package/dist/esm/query/ir.d.ts +1 -1
  117. package/dist/esm/query/ir.js.map +1 -1
  118. package/dist/esm/query/live/collection-config-builder.d.ts +3 -0
  119. package/dist/esm/query/live/collection-config-builder.js +29 -12
  120. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  121. package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
  122. package/dist/esm/query/live/collection-subscriber.js +43 -104
  123. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  124. package/dist/esm/query/live-query-collection.d.ts +1 -1
  125. package/dist/esm/query/live-query-collection.js +1 -1
  126. package/dist/esm/query/live-query-collection.js.map +1 -1
  127. package/dist/esm/transactions.js +3 -3
  128. package/dist/esm/transactions.js.map +1 -1
  129. package/dist/esm/types.d.ts +12 -10
  130. package/package.json +2 -2
  131. package/src/{change-events.ts → collection/change-events.ts} +25 -39
  132. package/src/collection/changes.ts +163 -0
  133. package/src/collection/events.ts +171 -0
  134. package/src/collection/index.ts +808 -0
  135. package/src/collection/indexes.ts +172 -0
  136. package/src/collection/lifecycle.ts +221 -0
  137. package/src/collection/mutations.ts +535 -0
  138. package/src/collection/state.ts +866 -0
  139. package/src/collection/subscription.ts +239 -0
  140. package/src/collection/sync.ts +235 -0
  141. package/src/index.ts +2 -2
  142. package/src/indexes/auto-index.ts +1 -1
  143. package/src/indexes/base-index.ts +3 -3
  144. package/src/indexes/btree-index.ts +2 -2
  145. package/src/query/builder/index.ts +1 -1
  146. package/src/query/builder/types.ts +1 -1
  147. package/src/query/compiler/index.ts +7 -1
  148. package/src/query/compiler/joins.ts +28 -41
  149. package/src/query/compiler/order-by.ts +1 -1
  150. package/src/query/ir.ts +1 -1
  151. package/src/query/live/collection-config-builder.ts +48 -22
  152. package/src/query/live/collection-subscriber.ts +63 -168
  153. package/src/query/live-query-collection.ts +2 -2
  154. package/src/transactions.ts +3 -3
  155. package/src/types.ts +14 -15
  156. package/dist/cjs/change-events.cjs.map +0 -1
  157. package/dist/cjs/collection.cjs +0 -1580
  158. package/dist/cjs/collection.cjs.map +0 -1
  159. package/dist/esm/change-events.js.map +0 -1
  160. package/dist/esm/collection.js +0 -1580
  161. package/dist/esm/collection.js.map +0 -1
  162. package/src/collection.ts +0 -2488
@@ -1,5 +1,4 @@
1
1
  import { MultiSet } from "@tanstack/db-ivm";
2
- import { createFilterFunctionFromExpression } from "../../change-events.js";
3
2
  import { convertToBasicExpression } from "../compiler/expressions.js";
4
3
  class CollectionSubscriber {
5
4
  constructor(collectionId, collection, config, syncState, collectionConfigBuilder) {
@@ -8,27 +7,7 @@ class CollectionSubscriber {
8
7
  this.config = config;
9
8
  this.syncState = syncState;
10
9
  this.collectionConfigBuilder = collectionConfigBuilder;
11
- this.sentKeys = /* @__PURE__ */ new Set();
12
10
  this.biggest = void 0;
13
- this.sendVisibleChangesToPipeline = (changes, loadedInitialState) => {
14
- if (loadedInitialState) {
15
- return this.sendChangesToPipeline(changes);
16
- }
17
- const newChanges = [];
18
- for (const change of changes) {
19
- let newChange = change;
20
- if (!this.sentKeys.has(change.key)) {
21
- if (change.type === `update`) {
22
- newChange = { ...change, type: `insert` };
23
- } else if (change.type === `delete`) {
24
- continue;
25
- }
26
- this.sentKeys.add(change.key);
27
- }
28
- newChanges.push(newChange);
29
- }
30
- return this.sendChangesToPipeline(newChanges);
31
- };
32
11
  }
33
12
  subscribe() {
34
13
  const collectionAlias = findCollectionAlias(
@@ -42,29 +21,35 @@ class CollectionSubscriber {
42
21
  collectionAlias
43
22
  );
44
23
  if (whereExpression) {
45
- this.subscribeToChanges(whereExpression);
24
+ return this.subscribeToChanges(whereExpression);
46
25
  } else {
47
26
  throw new Error(
48
27
  `Failed to convert WHERE clause to collection filter for collection '${this.collectionId}'. This indicates a bug in the query optimization logic.`
49
28
  );
50
29
  }
51
30
  } else {
52
- this.subscribeToChanges();
31
+ return this.subscribeToChanges();
53
32
  }
54
33
  }
55
34
  subscribeToChanges(whereExpression) {
56
- let unsubscribe;
57
- if (this.collectionConfigBuilder.lazyCollections.has(this.collectionId)) {
58
- unsubscribe = this.subscribeToMatchingChanges(whereExpression);
59
- } else if (Object.hasOwn(
35
+ let subscription;
36
+ if (Object.hasOwn(
60
37
  this.collectionConfigBuilder.optimizableOrderByCollections,
61
38
  this.collectionId
62
39
  )) {
63
- unsubscribe = this.subscribeToOrderedChanges(whereExpression);
40
+ subscription = this.subscribeToOrderedChanges(whereExpression);
64
41
  } else {
65
- unsubscribe = this.subscribeToAllChanges(whereExpression);
42
+ const includeInitialState = !this.collectionConfigBuilder.lazyCollections.has(this.collectionId);
43
+ subscription = this.subscribeToMatchingChanges(
44
+ whereExpression,
45
+ includeInitialState
46
+ );
66
47
  }
48
+ const unsubscribe = () => {
49
+ subscription.unsubscribe();
50
+ };
67
51
  this.syncState.unsubscribeCallbacks.add(unsubscribe);
52
+ return subscription;
68
53
  }
69
54
  sendChangesToPipeline(changes, callback) {
70
55
  const input = this.syncState.inputs[this.collectionId];
@@ -80,65 +65,18 @@ class CollectionSubscriber {
80
65
  dataLoader
81
66
  );
82
67
  }
83
- loadKeys(keys, filterFn) {
84
- const changes = [];
85
- for (const key of keys) {
86
- if (this.sentKeys.has(key)) continue;
87
- const value = this.collection.get(key);
88
- if (value !== void 0 && filterFn(value)) {
89
- this.sentKeys.add(key);
90
- changes.push({ type: `insert`, key, value });
91
- }
92
- }
93
- if (changes.length > 0) {
68
+ subscribeToMatchingChanges(whereExpression, includeInitialState = false) {
69
+ const sendChanges = (changes) => {
94
70
  this.sendChangesToPipeline(changes);
95
- }
96
- }
97
- subscribeToAllChanges(whereExpression) {
98
- const sendChangesToPipeline = this.sendChangesToPipeline.bind(this);
99
- const unsubscribe = this.collection.subscribeChanges(
100
- sendChangesToPipeline,
101
- {
102
- includeInitialState: true,
103
- ...whereExpression ? { whereExpression } : void 0
104
- }
105
- );
106
- return unsubscribe;
107
- }
108
- subscribeToMatchingChanges(whereExpression) {
109
- let loadedInitialState = false;
110
- let sendChanges = false;
111
- const sendVisibleChanges = (changes) => {
112
- this.sendVisibleChangesToPipeline(
113
- sendChanges ? changes : [],
114
- loadedInitialState
115
- );
116
71
  };
117
- const unsubscribe = this.collection.subscribeChanges(sendVisibleChanges, {
72
+ const subscription = this.collection.subscribeChanges(sendChanges, {
73
+ includeInitialState,
118
74
  whereExpression
119
75
  });
120
- const filterFn = whereExpression ? createFilterFunctionFromExpression(whereExpression) : () => true;
121
- const loadKs = (keys) => {
122
- sendChanges = true;
123
- return this.loadKeys(keys, filterFn);
124
- };
125
- this.collectionConfigBuilder.lazyCollectionsCallbacks[this.collectionId] = {
126
- loadKeys: loadKs,
127
- loadInitialState: () => {
128
- if (loadedInitialState) return;
129
- loadedInitialState = true;
130
- sendChanges = true;
131
- const changes = this.collection.currentStateAsChanges({
132
- whereExpression
133
- });
134
- this.sendChangesToPipeline(changes);
135
- }
136
- };
137
- return unsubscribe;
76
+ return subscription;
138
77
  }
139
78
  subscribeToOrderedChanges(whereExpression) {
140
- const { offset, limit, comparator, dataNeeded } = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
141
- this.loadNextItems(offset + limit);
79
+ const { offset, limit, comparator, dataNeeded, index } = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
142
80
  const sendChangesInRange = (changes) => {
143
81
  const splittedChanges = splitUpdates(changes);
144
82
  let filteredChanges = splittedChanges;
@@ -149,20 +87,21 @@ class CollectionSubscriber {
149
87
  this.biggest
150
88
  );
151
89
  }
152
- this.sendChangesToPipeline(
153
- filteredChanges,
154
- this.loadMoreIfNeeded.bind(this)
155
- );
90
+ this.sendChangesToPipelineWithTracking(filteredChanges, subscription);
156
91
  };
157
- const unsubscribe = this.collection.subscribeChanges(sendChangesInRange, {
92
+ const subscription = this.collection.subscribeChanges(sendChangesInRange, {
158
93
  whereExpression
159
94
  });
160
- return unsubscribe;
95
+ subscription.setOrderByIndex(index);
96
+ subscription.requestLimitedSnapshot({
97
+ limit: offset + limit
98
+ });
99
+ return subscription;
161
100
  }
162
101
  // This function is called by maybeRunGraph
163
102
  // after each iteration of the query pipeline
164
103
  // to ensure that the orderBy operator has enough data to work with
165
- loadMoreIfNeeded() {
104
+ loadMoreIfNeeded(subscription) {
166
105
  const orderByInfo = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
167
106
  if (!orderByInfo) {
168
107
  return true;
@@ -174,30 +113,29 @@ class CollectionSubscriber {
174
113
  );
175
114
  }
176
115
  const n = dataNeeded();
177
- let noMoreNextItems = false;
178
116
  if (n > 0) {
179
- const loadedItems = this.loadNextItems(n);
180
- noMoreNextItems = loadedItems === 0;
117
+ this.loadNextItems(n, subscription);
181
118
  }
182
- return n === 0 || noMoreNextItems;
119
+ return true;
183
120
  }
184
- sendChangesToPipelineWithTracking(changes) {
121
+ sendChangesToPipelineWithTracking(changes, subscription) {
185
122
  const { comparator } = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
186
123
  const trackedChanges = this.trackSentValues(changes, comparator);
187
- this.sendChangesToPipeline(trackedChanges, this.loadMoreIfNeeded.bind(this));
124
+ this.sendChangesToPipeline(
125
+ trackedChanges,
126
+ this.loadMoreIfNeeded.bind(this, subscription)
127
+ );
188
128
  }
189
129
  // Loads the next `n` items from the collection
190
130
  // starting from the biggest item it has sent
191
- loadNextItems(n) {
192
- const { valueExtractorForRawRow, index } = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
131
+ loadNextItems(n, subscription) {
132
+ const { valueExtractorForRawRow } = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
193
133
  const biggestSentRow = this.biggest;
194
134
  const biggestSentValue = biggestSentRow ? valueExtractorForRawRow(biggestSentRow) : biggestSentRow;
195
- const nextOrderedKeys = index.take(n, biggestSentValue);
196
- const nextInserts = nextOrderedKeys.map((key) => {
197
- return { type: `insert`, key, value: this.collection.get(key) };
135
+ subscription.requestLimitedSnapshot({
136
+ limit: n,
137
+ minValue: biggestSentValue
198
138
  });
199
- this.sendChangesToPipelineWithTracking(nextInserts);
200
- return nextInserts.length;
201
139
  }
202
140
  getWhereClauseFromAlias(collectionAlias) {
203
141
  const collectionWhereClausesCache = this.collectionConfigBuilder.collectionWhereClausesCache;
@@ -208,7 +146,6 @@ class CollectionSubscriber {
208
146
  }
209
147
  *trackSentValues(changes, comparator) {
210
148
  for (const change of changes) {
211
- this.sentKeys.add(change.key);
212
149
  if (!this.biggest) {
213
150
  this.biggest = change.value;
214
151
  } else if (comparator(this.biggest, change.value) < 0) {
@@ -245,7 +182,9 @@ function sendChangesToInput(input, changes, getKey) {
245
182
  multiSetArray.push([[key, change.value], -1]);
246
183
  }
247
184
  }
248
- input.sendData(new MultiSet(multiSetArray));
185
+ if (multiSetArray.length !== 0) {
186
+ input.sendData(new MultiSet(multiSetArray));
187
+ }
249
188
  return multiSetArray.length;
250
189
  }
251
190
  function* splitUpdates(changes) {
@@ -1 +1 @@
1
- {"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet } from \"@tanstack/db-ivm\"\nimport { createFilterFunctionFromExpression } from \"../../change-events.js\"\nimport { convertToBasicExpression } from \"../compiler/expressions.js\"\nimport type { FullSyncState } from \"./types.js\"\nimport type { MultiSetArray, RootStreamBuilder } from \"@tanstack/db-ivm\"\nimport type { Collection } from \"../../collection.js\"\nimport type { ChangeMessage, SyncConfig } from \"../../types.js\"\nimport type { Context, GetResult } from \"../builder/types.js\"\nimport type { BasicExpression } from \"../ir.js\"\nimport type { CollectionConfigBuilder } from \"./collection-config-builder.js\"\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the keys we've sent (needed for join and orderBy optimizations)\n private sentKeys = new Set<string | number>()\n\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n constructor(\n private collectionId: string,\n private collection: Collection,\n private config: Parameters<SyncConfig<TResult>[`sync`]>[0],\n private syncState: FullSyncState,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>\n ) {}\n\n subscribe() {\n const collectionAlias = findCollectionAlias(\n this.collectionId,\n this.collectionConfigBuilder.query\n )\n const whereClause = this.getWhereClauseFromAlias(collectionAlias)\n\n if (whereClause) {\n // Convert WHERE clause to BasicExpression format for collection subscription\n const whereExpression = convertToBasicExpression(\n whereClause,\n collectionAlias!\n )\n\n if (whereExpression) {\n // Use index optimization for this collection\n this.subscribeToChanges(whereExpression)\n } else {\n // This should not happen - if we have a whereClause but can't create whereExpression,\n // it indicates a bug in our optimization logic\n throw new Error(\n `Failed to convert WHERE clause to collection filter for collection '${this.collectionId}'. ` +\n `This indicates a bug in the query optimization logic.`\n )\n }\n } else {\n // No WHERE clause for this collection, use regular subscription\n this.subscribeToChanges()\n }\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n let unsubscribe: () => void\n if (this.collectionConfigBuilder.lazyCollections.has(this.collectionId)) {\n unsubscribe = this.subscribeToMatchingChanges(whereExpression)\n } else if (\n Object.hasOwn(\n this.collectionConfigBuilder.optimizableOrderByCollections,\n this.collectionId\n )\n ) {\n unsubscribe = this.subscribeToOrderedChanges(whereExpression)\n } else {\n unsubscribe = this.subscribeToAllChanges(whereExpression)\n }\n this.syncState.unsubscribeCallbacks.add(unsubscribe)\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean\n ) {\n const input = this.syncState.inputs[this.collectionId]!\n const sentChanges = sendChangesToInput(\n input,\n changes,\n this.collection.config.getKey\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to call `maybeRunGraph` even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `maybeRunGraph`\n this.collectionConfigBuilder.maybeRunGraph(\n this.config,\n this.syncState,\n dataLoader\n )\n }\n\n // Wraps the sendChangesToPipeline function\n // in order to turn `update`s into `insert`s\n // for keys that have not been sent to the pipeline yet\n // and filter out deletes for keys that have not been sent\n private sendVisibleChangesToPipeline = (\n changes: Array<ChangeMessage<any, string | number>>,\n loadedInitialState: boolean\n ) => {\n if (loadedInitialState) {\n // There was no index for the join key\n // so we loaded the initial state\n // so we can safely assume that the pipeline has seen all keys\n return this.sendChangesToPipeline(changes)\n }\n\n const newChanges = []\n for (const change of changes) {\n let newChange = change\n if (!this.sentKeys.has(change.key)) {\n if (change.type === `update`) {\n newChange = { ...change, type: `insert` }\n } else if (change.type === `delete`) {\n // filter out deletes for keys that have not been sent\n continue\n }\n this.sentKeys.add(change.key)\n }\n newChanges.push(newChange)\n }\n\n return this.sendChangesToPipeline(newChanges)\n }\n\n private loadKeys(\n keys: Iterable<string | number>,\n filterFn: (item: object) => boolean\n ) {\n const changes: Array<ChangeMessage<any, string | number>> = []\n for (const key of keys) {\n // Only load the key once\n if (this.sentKeys.has(key)) continue\n\n const value = this.collection.get(key)\n if (value !== undefined && filterFn(value)) {\n this.sentKeys.add(key)\n changes.push({ type: `insert`, key, value })\n }\n }\n if (changes.length > 0) {\n this.sendChangesToPipeline(changes)\n }\n }\n\n private subscribeToAllChanges(\n whereExpression: BasicExpression<boolean> | undefined\n ) {\n const sendChangesToPipeline = this.sendChangesToPipeline.bind(this)\n const unsubscribe = this.collection.subscribeChanges(\n sendChangesToPipeline,\n {\n includeInitialState: true,\n ...(whereExpression ? { whereExpression } : undefined),\n }\n )\n return unsubscribe\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined\n ) {\n // Flag to indicate we have send to whole initial state of the collection\n // to the pipeline, this is set when there are no indexes that can be used\n // to filter the changes and so the whole state was requested from the collection\n let loadedInitialState = false\n\n // Flag to indicate that we have started sending changes to the pipeline.\n // This is set to true by either the first call to `loadKeys` or when the\n // query requests the whole initial state in `loadInitialState`.\n // Until that point we filter out all changes from subscription to the collection.\n let sendChanges = false\n\n const sendVisibleChanges = (\n changes: Array<ChangeMessage<any, string | number>>\n ) => {\n // We filter out changes when sendChanges is false to ensure that we don't send\n // any changes from the live subscription until the join operator requests either\n // the initial state or its first key. This is needed otherwise it could receive\n // changes which are then later subsumed by the initial state (and that would\n // lead to weird bugs due to the data being received twice).\n this.sendVisibleChangesToPipeline(\n sendChanges ? changes : [],\n loadedInitialState\n )\n }\n\n const unsubscribe = this.collection.subscribeChanges(sendVisibleChanges, {\n whereExpression,\n })\n\n // Create a function that loads keys from the collection\n // into the query pipeline on demand\n const filterFn = whereExpression\n ? createFilterFunctionFromExpression(whereExpression)\n : () => true\n const loadKs = (keys: Set<string | number>) => {\n sendChanges = true\n return this.loadKeys(keys, filterFn)\n }\n\n // Store the functions to load keys and load initial state in the `lazyCollectionsCallbacks` map\n // This is used by the join operator to dynamically load matching keys from the lazy collection\n // or to get the full initial state of the collection if there's no index for the join key\n this.collectionConfigBuilder.lazyCollectionsCallbacks[this.collectionId] = {\n loadKeys: loadKs,\n loadInitialState: () => {\n // Make sure we only load the initial state once\n if (loadedInitialState) return\n loadedInitialState = true\n sendChanges = true\n\n const changes = this.collection.currentStateAsChanges({\n whereExpression,\n })\n this.sendChangesToPipeline(changes)\n },\n }\n return unsubscribe\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined\n ) {\n const { offset, limit, comparator, dataNeeded } =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]!\n\n // Load the first `offset + limit` values from the index\n // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[\n this.loadNextItems(offset + limit)\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>\n ) => {\n // Split live updates into a delete of the old value and an insert of the new value\n // and filter out changes that are bigger than the biggest value we've sent so far\n // because they can't affect the topK\n const splittedChanges = splitUpdates(changes)\n let filteredChanges = splittedChanges\n if (dataNeeded!() === 0) {\n // If the topK is full [..., maxSentValue] then we do not need to send changes > maxSentValue\n // because they can never make it into the topK.\n // However, if the topK isn't full yet, we need to also send changes > maxSentValue\n // because they will make it into the topK\n filteredChanges = filterChangesSmallerOrEqualToMax(\n splittedChanges,\n comparator,\n this.biggest\n )\n }\n this.sendChangesToPipeline(\n filteredChanges,\n this.loadMoreIfNeeded.bind(this)\n )\n }\n\n // Subscribe to changes and only send changes that are smaller than the biggest value we've sent so far\n // values that are bigger don't need to be sent because they can't affect the topK\n const unsubscribe = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n })\n\n return unsubscribe\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded() {\n const orderByInfo =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load, just return true\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // This should never happen because the topK operator should always set the size callback\n // which in turn should lead to the orderBy operator setting the dataNeeded callback\n throw new Error(\n `Missing dataNeeded callback for collection ${this.collectionId}`\n )\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n let noMoreNextItems = false\n if (n > 0) {\n const loadedItems = this.loadNextItems(n)\n noMoreNextItems = loadedItems === 0\n }\n\n // Indicate that we're done loading data if we didn't need to load more data\n // or there's no more data to load\n return n === 0 || noMoreNextItems\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>\n ) {\n const { comparator } =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]!\n const trackedChanges = this.trackSentValues(changes, comparator)\n this.sendChangesToPipeline(trackedChanges, this.loadMoreIfNeeded.bind(this))\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number) {\n const { valueExtractorForRawRow, index } =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]!\n const biggestSentRow = this.biggest\n const biggestSentValue = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : biggestSentRow\n // Take the `n` items after the biggest sent value\n const nextOrderedKeys = index.take(n, biggestSentValue)\n const nextInserts: Array<ChangeMessage<any, string | number>> =\n nextOrderedKeys.map((key) => {\n return { type: `insert`, key, value: this.collection.get(key) }\n })\n this.sendChangesToPipelineWithTracking(nextInserts)\n return nextInserts.length\n }\n\n private getWhereClauseFromAlias(\n collectionAlias: string | undefined\n ): BasicExpression<boolean> | undefined {\n const collectionWhereClausesCache =\n this.collectionConfigBuilder.collectionWhereClausesCache\n if (collectionAlias && collectionWhereClausesCache) {\n return collectionWhereClausesCache.get(collectionAlias)\n }\n return undefined\n }\n\n private *trackSentValues(\n changes: Iterable<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number\n ) {\n for (const change of changes) {\n this.sentKeys.add(change.key)\n\n if (!this.biggest) {\n this.biggest = change.value\n } else if (comparator(this.biggest, change.value) < 0) {\n this.biggest = change.value\n }\n\n yield change\n }\n }\n}\n\n/**\n * Finds the alias for a collection ID in the query\n */\nfunction findCollectionAlias(\n collectionId: string,\n query: any\n): string | undefined {\n // Check FROM clause\n if (\n query.from?.type === `collectionRef` &&\n query.from.collection?.id === collectionId\n ) {\n return query.from.alias\n }\n\n // Check JOIN clauses\n if (query.join) {\n for (const joinClause of query.join) {\n if (\n joinClause.from?.type === `collectionRef` &&\n joinClause.from.collection?.id === collectionId\n ) {\n return joinClause.from.alias\n }\n }\n }\n\n return undefined\n}\n\n/**\n * Helper function to send changes to a D2 input stream\n */\nfunction sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n input.sendData(new MultiSet(multiSetArray))\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nfunction* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n\nfunction* filterChanges<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n f: (change: ChangeMessage<T, TKey>) => boolean\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (f(change)) {\n yield change\n }\n }\n}\n\n/**\n * Filters changes to only include those that are smaller than the provided max value\n * @param changes - Iterable of changes to filter\n * @param comparator - Comparator function to use for filtering\n * @param maxValue - Range to filter changes within (range boundaries are exclusive)\n * @returns Iterable of changes that fall within the range\n */\nfunction* filterChangesSmallerOrEqualToMax<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n comparator: (a: any, b: any) => number,\n maxValue: any\n): Generator<ChangeMessage<T, TKey>> {\n yield* filterChanges(changes, (change) => {\n return !maxValue || comparator(change.value, maxValue) <= 0\n })\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet } from \"@tanstack/db-ivm\"\nimport { convertToBasicExpression } from \"../compiler/expressions.js\"\nimport type { FullSyncState } from \"./types.js\"\nimport type { MultiSetArray, RootStreamBuilder } from \"@tanstack/db-ivm\"\nimport type { Collection } from \"../../collection/index.js\"\nimport type { ChangeMessage, SyncConfig } from \"../../types.js\"\nimport type { Context, GetResult } from \"../builder/types.js\"\nimport type { BasicExpression } from \"../ir.js\"\nimport type { CollectionConfigBuilder } from \"./collection-config-builder.js\"\nimport type { CollectionSubscription } from \"../../collection/subscription.js\"\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n constructor(\n private collectionId: string,\n private collection: Collection,\n private config: Parameters<SyncConfig<TResult>[`sync`]>[0],\n private syncState: FullSyncState,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>\n ) {}\n\n subscribe(): CollectionSubscription {\n const collectionAlias = findCollectionAlias(\n this.collectionId,\n this.collectionConfigBuilder.query\n )\n const whereClause = this.getWhereClauseFromAlias(collectionAlias)\n\n if (whereClause) {\n // Convert WHERE clause to BasicExpression format for collection subscription\n const whereExpression = convertToBasicExpression(\n whereClause,\n collectionAlias!\n )\n\n if (whereExpression) {\n // Use index optimization for this collection\n return this.subscribeToChanges(whereExpression)\n } else {\n // This should not happen - if we have a whereClause but can't create whereExpression,\n // it indicates a bug in our optimization logic\n throw new Error(\n `Failed to convert WHERE clause to collection filter for collection '${this.collectionId}'. ` +\n `This indicates a bug in the query optimization logic.`\n )\n }\n } else {\n // No WHERE clause for this collection, use regular subscription\n return this.subscribeToChanges()\n }\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n let subscription: CollectionSubscription\n if (\n Object.hasOwn(\n this.collectionConfigBuilder.optimizableOrderByCollections,\n this.collectionId\n )\n ) {\n subscription = this.subscribeToOrderedChanges(whereExpression)\n } else {\n // If the collection is lazy then we should not include the initial state\n const includeInitialState =\n !this.collectionConfigBuilder.lazyCollections.has(this.collectionId)\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState\n )\n }\n const unsubscribe = () => {\n subscription.unsubscribe()\n }\n this.syncState.unsubscribeCallbacks.add(unsubscribe)\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean\n ) {\n const input = this.syncState.inputs[this.collectionId]!\n const sentChanges = sendChangesToInput(\n input,\n changes,\n this.collection.config.getKey\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to call `maybeRunGraph` even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `maybeRunGraph`\n this.collectionConfigBuilder.maybeRunGraph(\n this.config,\n this.syncState,\n dataLoader\n )\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n includeInitialState: boolean = false\n ) {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n const subscription = this.collection.subscribeChanges(sendChanges, {\n includeInitialState,\n whereExpression,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined\n ) {\n const { offset, limit, comparator, dataNeeded, index } =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]!\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>\n ) => {\n // Split live updates into a delete of the old value and an insert of the new value\n // and filter out changes that are bigger than the biggest value we've sent so far\n // because they can't affect the topK (and if later we need more data, we will dynamically load more data)\n const splittedChanges = splitUpdates(changes)\n let filteredChanges = splittedChanges\n if (dataNeeded!() === 0) {\n // If the topK is full [..., maxSentValue] then we do not need to send changes > maxSentValue\n // because they can never make it into the topK.\n // However, if the topK isn't full yet, we need to also send changes > maxSentValue\n // because they will make it into the topK\n filteredChanges = filterChangesSmallerOrEqualToMax(\n splittedChanges,\n comparator,\n this.biggest\n )\n }\n\n this.sendChangesToPipelineWithTracking(filteredChanges, subscription)\n }\n\n // Subscribe to changes and only send changes that are smaller than the biggest value we've sent so far\n // values that are bigger don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n })\n\n subscription.setOrderByIndex(index)\n\n // Load the first `offset + limit` values from the index\n // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[\n subscription.requestLimitedSnapshot({\n limit: offset + limit,\n })\n\n return subscription\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded(subscription: CollectionSubscription) {\n const orderByInfo =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // This should never happen because the topK operator should always set the size callback\n // which in turn should lead to the orderBy operator setting the dataNeeded callback\n throw new Error(\n `Missing dataNeeded callback for collection ${this.collectionId}`\n )\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n if (n > 0) {\n this.loadNextItems(n, subscription)\n }\n return true\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>,\n subscription: CollectionSubscription\n ) {\n const { comparator } =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]!\n const trackedChanges = this.trackSentValues(changes, comparator)\n this.sendChangesToPipeline(\n trackedChanges,\n this.loadMoreIfNeeded.bind(this, subscription)\n )\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number, subscription: CollectionSubscription) {\n const { valueExtractorForRawRow } =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]!\n const biggestSentRow = this.biggest\n const biggestSentValue = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : biggestSentRow\n // Take the `n` items after the biggest sent value\n subscription.requestLimitedSnapshot({\n limit: n,\n minValue: biggestSentValue,\n })\n }\n\n private getWhereClauseFromAlias(\n collectionAlias: string | undefined\n ): BasicExpression<boolean> | undefined {\n const collectionWhereClausesCache =\n this.collectionConfigBuilder.collectionWhereClausesCache\n if (collectionAlias && collectionWhereClausesCache) {\n return collectionWhereClausesCache.get(collectionAlias)\n }\n return undefined\n }\n\n private *trackSentValues(\n changes: Iterable<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number\n ) {\n for (const change of changes) {\n if (!this.biggest) {\n this.biggest = change.value\n } else if (comparator(this.biggest, change.value) < 0) {\n this.biggest = change.value\n }\n\n yield change\n }\n }\n}\n\n/**\n * Finds the alias for a collection ID in the query\n */\nfunction findCollectionAlias(\n collectionId: string,\n query: any\n): string | undefined {\n // Check FROM clause\n if (\n query.from?.type === `collectionRef` &&\n query.from.collection?.id === collectionId\n ) {\n return query.from.alias\n }\n\n // Check JOIN clauses\n if (query.join) {\n for (const joinClause of query.join) {\n if (\n joinClause.from?.type === `collectionRef` &&\n joinClause.from.collection?.id === collectionId\n ) {\n return joinClause.from.alias\n }\n }\n }\n\n return undefined\n}\n\n/**\n * Helper function to send changes to a D2 input stream\n */\nfunction sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n\n if (multiSetArray.length !== 0) {\n input.sendData(new MultiSet(multiSetArray))\n }\n\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nfunction* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n\nfunction* filterChanges<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n f: (change: ChangeMessage<T, TKey>) => boolean\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (f(change)) {\n yield change\n }\n }\n}\n\n/**\n * Filters changes to only include those that are smaller or equal to the provided max value\n * @param changes - Iterable of changes to filter\n * @param comparator - Comparator function to use for filtering\n * @param maxValue - Range to filter changes within (range boundaries are exclusive)\n * @returns Iterable of changes that fall within the range\n */\nfunction* filterChangesSmallerOrEqualToMax<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n comparator: (a: any, b: any) => number,\n maxValue: any\n): Generator<ChangeMessage<T, TKey>> {\n yield* filterChanges(changes, (change) => {\n return !maxValue || comparator(change.value, maxValue) <= 0\n })\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { LiveQueryCollectionConfig } from './live/types.js';
2
2
  import { InitialQueryBuilder, QueryBuilder } from './builder/index.js';
3
- import { Collection } from '../collection.js';
3
+ import { Collection } from '../collection/index.js';
4
4
  import { CollectionConfig, UtilsRecord } from '../types.js';
5
5
  import { Context, GetResult } from './builder/types.js';
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { createCollection } from "../collection.js";
1
+ import { createCollection } from "../collection/index.js";
2
2
  import { CollectionConfigBuilder } from "./live/collection-config-builder.js";
3
3
  function liveQueryCollectionOptions(config) {
4
4
  const collectionConfigBuilder = new CollectionConfigBuilder(config);
@@ -1 +1 @@
1
- {"version":3,"file":"live-query-collection.js","sources":["../../../src/query/live-query-collection.ts"],"sourcesContent":["import { createCollection } from \"../collection.js\"\nimport { CollectionConfigBuilder } from \"./live/collection-config-builder.js\"\nimport type { LiveQueryCollectionConfig } from \"./live/types.js\"\nimport type { InitialQueryBuilder, QueryBuilder } from \"./builder/index.js\"\nimport type { Collection } from \"../collection.js\"\nimport type { CollectionConfig, UtilsRecord } from \"../types.js\"\nimport type { Context, GetResult } from \"./builder/types.js\"\n\n/**\n * Creates live query collection options for use with createCollection\n *\n * @example\n * ```typescript\n * const options = liveQueryCollectionOptions({\n * // id is optional - will auto-generate if not provided\n * query: (q) => q\n * .from({ post: postsCollection })\n * .where(({ post }) => eq(post.published, true))\n * .select(({ post }) => ({\n * id: post.id,\n * title: post.title,\n * content: post.content,\n * })),\n * // getKey is optional - will use stream key if not provided\n * })\n *\n * const collection = createCollection(options)\n * ```\n *\n * @param config - Configuration options for the live query collection\n * @returns Collection options that can be passed to createCollection\n */\nexport function liveQueryCollectionOptions<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n>(\n config: LiveQueryCollectionConfig<TContext, TResult>\n): CollectionConfig<TResult> {\n const collectionConfigBuilder = new CollectionConfigBuilder<\n TContext,\n TResult\n >(config)\n return collectionConfigBuilder.getConfig()\n}\n\n/**\n * Creates a live query collection directly\n *\n * @example\n * ```typescript\n * // Minimal usage - just pass a query function\n * const activeUsers = createLiveQueryCollection(\n * (q) => q\n * .from({ user: usersCollection })\n * .where(({ user }) => eq(user.active, true))\n * .select(({ user }) => ({ id: user.id, name: user.name }))\n * )\n *\n * // Full configuration with custom options\n * const searchResults = createLiveQueryCollection({\n * id: \"search-results\", // Custom ID (auto-generated if omitted)\n * query: (q) => q\n * .from({ post: postsCollection })\n * .where(({ post }) => like(post.title, `%${searchTerm}%`))\n * .select(({ post }) => ({\n * id: post.id,\n * title: post.title,\n * excerpt: post.excerpt,\n * })),\n * getKey: (item) => item.id, // Custom key function (uses stream key if omitted)\n * utils: {\n * updateSearchTerm: (newTerm: string) => {\n * // Custom utility functions\n * }\n * }\n * })\n * ```\n */\n\n// Overload 1: Accept just the query function\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n>(\n query: (q: InitialQueryBuilder) => QueryBuilder<TContext>\n): Collection<TResult, string | number, {}>\n\n// Overload 2: Accept full config object with optional utilities\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n TUtils extends UtilsRecord = {},\n>(\n config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }\n): Collection<TResult, string | number, TUtils>\n\n// Implementation\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n TUtils extends UtilsRecord = {},\n>(\n configOrQuery:\n | (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })\n | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)\n): Collection<TResult, string | number, TUtils> {\n // Determine if the argument is a function (query) or a config object\n if (typeof configOrQuery === `function`) {\n // Simple query function case\n const config: LiveQueryCollectionConfig<TContext, TResult> = {\n query: configOrQuery as (\n q: InitialQueryBuilder\n ) => QueryBuilder<TContext>,\n }\n const options = liveQueryCollectionOptions<TContext, TResult>(config)\n return bridgeToCreateCollection(options)\n } else {\n // Config object case\n const config = configOrQuery as LiveQueryCollectionConfig<\n TContext,\n TResult\n > & { utils?: TUtils }\n const options = liveQueryCollectionOptions<TContext, TResult>(config)\n return bridgeToCreateCollection({\n ...options,\n utils: config.utils,\n })\n }\n}\n\n/**\n * Bridge function that handles the type compatibility between query2's TResult\n * and core collection's output type without exposing ugly type assertions to users\n */\nfunction bridgeToCreateCollection<\n TResult extends object,\n TUtils extends UtilsRecord = {},\n>(\n options: CollectionConfig<TResult> & { utils?: TUtils }\n): Collection<TResult, string | number, TUtils> {\n // This is the only place we need a type assertion, hidden from user API\n return createCollection(options as any) as unknown as Collection<\n TResult,\n string | number,\n TUtils\n >\n}\n"],"names":[],"mappings":";;AAgCO,SAAS,2BAId,QAC2B;AAC3B,QAAM,0BAA0B,IAAI,wBAGlC,MAAM;AACR,SAAO,wBAAwB,UAAA;AACjC;AAsDO,SAAS,0BAKd,eAG8C;AAE9C,MAAI,OAAO,kBAAkB,YAAY;AAEvC,UAAM,SAAuD;AAAA,MAC3D,OAAO;AAAA,IAAA;AAIT,UAAM,UAAU,2BAA8C,MAAM;AACpE,WAAO,yBAAyB,OAAO;AAAA,EACzC,OAAO;AAEL,UAAM,SAAS;AAIf,UAAM,UAAU,2BAA8C,MAAM;AACpE,WAAO,yBAAyB;AAAA,MAC9B,GAAG;AAAA,MACH,OAAO,OAAO;AAAA,IAAA,CACf;AAAA,EACH;AACF;AAMA,SAAS,yBAIP,SAC8C;AAE9C,SAAO,iBAAiB,OAAc;AAKxC;"}
1
+ {"version":3,"file":"live-query-collection.js","sources":["../../../src/query/live-query-collection.ts"],"sourcesContent":["import { createCollection } from \"../collection/index.js\"\nimport { CollectionConfigBuilder } from \"./live/collection-config-builder.js\"\nimport type { LiveQueryCollectionConfig } from \"./live/types.js\"\nimport type { InitialQueryBuilder, QueryBuilder } from \"./builder/index.js\"\nimport type { Collection } from \"../collection/index.js\"\nimport type { CollectionConfig, UtilsRecord } from \"../types.js\"\nimport type { Context, GetResult } from \"./builder/types.js\"\n\n/**\n * Creates live query collection options for use with createCollection\n *\n * @example\n * ```typescript\n * const options = liveQueryCollectionOptions({\n * // id is optional - will auto-generate if not provided\n * query: (q) => q\n * .from({ post: postsCollection })\n * .where(({ post }) => eq(post.published, true))\n * .select(({ post }) => ({\n * id: post.id,\n * title: post.title,\n * content: post.content,\n * })),\n * // getKey is optional - will use stream key if not provided\n * })\n *\n * const collection = createCollection(options)\n * ```\n *\n * @param config - Configuration options for the live query collection\n * @returns Collection options that can be passed to createCollection\n */\nexport function liveQueryCollectionOptions<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n>(\n config: LiveQueryCollectionConfig<TContext, TResult>\n): CollectionConfig<TResult> {\n const collectionConfigBuilder = new CollectionConfigBuilder<\n TContext,\n TResult\n >(config)\n return collectionConfigBuilder.getConfig()\n}\n\n/**\n * Creates a live query collection directly\n *\n * @example\n * ```typescript\n * // Minimal usage - just pass a query function\n * const activeUsers = createLiveQueryCollection(\n * (q) => q\n * .from({ user: usersCollection })\n * .where(({ user }) => eq(user.active, true))\n * .select(({ user }) => ({ id: user.id, name: user.name }))\n * )\n *\n * // Full configuration with custom options\n * const searchResults = createLiveQueryCollection({\n * id: \"search-results\", // Custom ID (auto-generated if omitted)\n * query: (q) => q\n * .from({ post: postsCollection })\n * .where(({ post }) => like(post.title, `%${searchTerm}%`))\n * .select(({ post }) => ({\n * id: post.id,\n * title: post.title,\n * excerpt: post.excerpt,\n * })),\n * getKey: (item) => item.id, // Custom key function (uses stream key if omitted)\n * utils: {\n * updateSearchTerm: (newTerm: string) => {\n * // Custom utility functions\n * }\n * }\n * })\n * ```\n */\n\n// Overload 1: Accept just the query function\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n>(\n query: (q: InitialQueryBuilder) => QueryBuilder<TContext>\n): Collection<TResult, string | number, {}>\n\n// Overload 2: Accept full config object with optional utilities\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n TUtils extends UtilsRecord = {},\n>(\n config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }\n): Collection<TResult, string | number, TUtils>\n\n// Implementation\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n TUtils extends UtilsRecord = {},\n>(\n configOrQuery:\n | (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })\n | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)\n): Collection<TResult, string | number, TUtils> {\n // Determine if the argument is a function (query) or a config object\n if (typeof configOrQuery === `function`) {\n // Simple query function case\n const config: LiveQueryCollectionConfig<TContext, TResult> = {\n query: configOrQuery as (\n q: InitialQueryBuilder\n ) => QueryBuilder<TContext>,\n }\n const options = liveQueryCollectionOptions<TContext, TResult>(config)\n return bridgeToCreateCollection(options)\n } else {\n // Config object case\n const config = configOrQuery as LiveQueryCollectionConfig<\n TContext,\n TResult\n > & { utils?: TUtils }\n const options = liveQueryCollectionOptions<TContext, TResult>(config)\n return bridgeToCreateCollection({\n ...options,\n utils: config.utils,\n })\n }\n}\n\n/**\n * Bridge function that handles the type compatibility between query2's TResult\n * and core collection's output type without exposing ugly type assertions to users\n */\nfunction bridgeToCreateCollection<\n TResult extends object,\n TUtils extends UtilsRecord = {},\n>(\n options: CollectionConfig<TResult> & { utils?: TUtils }\n): Collection<TResult, string | number, TUtils> {\n // This is the only place we need a type assertion, hidden from user API\n return createCollection(options as any) as unknown as Collection<\n TResult,\n string | number,\n TUtils\n >\n}\n"],"names":[],"mappings":";;AAgCO,SAAS,2BAId,QAC2B;AAC3B,QAAM,0BAA0B,IAAI,wBAGlC,MAAM;AACR,SAAO,wBAAwB,UAAA;AACjC;AAsDO,SAAS,0BAKd,eAG8C;AAE9C,MAAI,OAAO,kBAAkB,YAAY;AAEvC,UAAM,SAAuD;AAAA,MAC3D,OAAO;AAAA,IAAA;AAIT,UAAM,UAAU,2BAA8C,MAAM;AACpE,WAAO,yBAAyB,OAAO;AAAA,EACzC,OAAO;AAEL,UAAM,SAAS;AAIf,UAAM,UAAU,2BAA8C,MAAM;AACpE,WAAO,yBAAyB;AAAA,MAC9B,GAAG;AAAA,MACH,OAAO,OAAO;AAAA,IAAA,CACf;AAAA,EACH;AACF;AAMA,SAAS,yBAIP,SAC8C;AAE9C,SAAO,iBAAiB,OAAc;AAKxC;"}
@@ -244,9 +244,9 @@ class Transaction {
244
244
  const hasCalled = /* @__PURE__ */ new Set();
245
245
  for (const mutation of this.mutations) {
246
246
  if (!hasCalled.has(mutation.collection.id)) {
247
- mutation.collection.onTransactionStateChange();
248
- if (mutation.collection.pendingSyncedTransactions.length > 0) {
249
- mutation.collection.commitPendingTransactions();
247
+ mutation.collection._state.onTransactionStateChange();
248
+ if (mutation.collection._state.pendingSyncedTransactions.length > 0) {
249
+ mutation.collection._state.commitPendingTransactions();
250
250
  }
251
251
  hasCalled.add(mutation.collection.id);
252
252
  }
@@ -1 +1 @@
1
- {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport {\n MissingMutationFunctionError,\n TransactionAlreadyCompletedRollbackError,\n TransactionNotPendingCommitError,\n TransactionNotPendingMutateError,\n} from \"./errors\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Merges two pending mutations for the same item within a transaction\n *\n * Merge behavior truth table:\n * - (insert, update) → insert (merge changes, keep empty original)\n * - (insert, delete) → null (cancel both mutations)\n * - (update, delete) → delete (delete dominates)\n * - (update, update) → update (replace with latest, union changes)\n * - (delete, delete) → delete (replace with latest)\n * - (insert, insert) → insert (replace with latest)\n *\n * Note: (delete, update) and (delete, insert) should never occur as the collection\n * layer prevents operations on deleted items within the same transaction.\n *\n * @param existing - The existing mutation in the transaction\n * @param incoming - The new mutation being applied\n * @returns The merged mutation, or null if both should be removed\n */\nfunction mergePendingMutations<T extends object>(\n existing: PendingMutation<T>,\n incoming: PendingMutation<T>\n): PendingMutation<T> | null {\n // Truth table implementation\n switch (`${existing.type}-${incoming.type}` as const) {\n case `insert-update`: {\n // Update after insert: keep as insert but merge changes\n // For insert-update, the key should remain the same since collections don't allow key changes\n return {\n ...existing,\n type: `insert` as const,\n original: {},\n modified: incoming.modified,\n changes: { ...existing.changes, ...incoming.changes },\n // Keep existing keys (key changes not allowed in updates)\n key: existing.key,\n globalKey: existing.globalKey,\n // Merge metadata (last-write-wins)\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n // Update tracking info\n mutationId: incoming.mutationId,\n updatedAt: incoming.updatedAt,\n }\n }\n\n case `insert-delete`:\n // Delete after insert: cancel both mutations\n return null\n\n case `update-delete`:\n // Delete after update: delete dominates\n return incoming\n\n case `update-update`: {\n // Update after update: replace with latest, union changes\n return {\n ...incoming,\n // Keep original from first update\n original: existing.original,\n // Union the changes from both updates\n changes: { ...existing.changes, ...incoming.changes },\n // Merge metadata\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n }\n }\n\n case `delete-delete`:\n case `insert-insert`:\n // Same type: replace with latest\n return incoming\n\n default: {\n // Exhaustiveness check\n const _exhaustive: never = `${existing.type}-${incoming.type}` as never\n throw new Error(`Unhandled mutation combination: ${_exhaustive}`)\n }\n }\n}\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw new MissingMutationFunctionError()\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingMutateError()\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit().catch(() => {\n // Errors from autoCommit are handled via isPersisted.promise\n // This catch prevents unhandled promise rejections\n })\n }\n\n return this\n }\n\n /**\n * Apply new mutations to this transaction, intelligently merging with existing mutations\n *\n * When mutations operate on the same item (same globalKey), they are merged according to\n * the following rules:\n *\n * - **insert + update** → insert (merge changes, keep empty original)\n * - **insert + delete** → removed (mutations cancel each other out)\n * - **update + delete** → delete (delete dominates)\n * - **update + update** → update (union changes, keep first original)\n * - **same type** → replace with latest\n *\n * This merging reduces over-the-wire churn and keeps the optimistic local view\n * aligned with user intent.\n *\n * @param mutations - Array of new mutations to apply\n */\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n const existingMutation = this.mutations[existingIndex]!\n const mergeResult = mergePendingMutations(existingMutation, newMutation)\n\n if (mergeResult === null) {\n // Remove the mutation (e.g., delete after insert cancels both)\n this.mutations.splice(existingIndex, 1)\n } else {\n // Replace with merged mutation\n this.mutations[existingIndex] = mergeResult\n }\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw new TransactionAlreadyCompletedRollbackError()\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingCommitError()\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n this.isPersisted.resolve(this)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Preserve the original error for rethrowing\n const originalError =\n error instanceof Error ? error : new Error(String(error))\n\n // Update transaction with error information\n this.error = {\n message: originalError.message,\n error: originalError,\n }\n\n // rollback the transaction\n this.rollback()\n\n // Re-throw the original error to preserve identity and stack\n throw originalError\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport {\n MissingMutationFunctionError,\n TransactionAlreadyCompletedRollbackError,\n TransactionNotPendingCommitError,\n TransactionNotPendingMutateError,\n} from \"./errors\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Merges two pending mutations for the same item within a transaction\n *\n * Merge behavior truth table:\n * - (insert, update) → insert (merge changes, keep empty original)\n * - (insert, delete) → null (cancel both mutations)\n * - (update, delete) → delete (delete dominates)\n * - (update, update) → update (replace with latest, union changes)\n * - (delete, delete) → delete (replace with latest)\n * - (insert, insert) → insert (replace with latest)\n *\n * Note: (delete, update) and (delete, insert) should never occur as the collection\n * layer prevents operations on deleted items within the same transaction.\n *\n * @param existing - The existing mutation in the transaction\n * @param incoming - The new mutation being applied\n * @returns The merged mutation, or null if both should be removed\n */\nfunction mergePendingMutations<T extends object>(\n existing: PendingMutation<T>,\n incoming: PendingMutation<T>\n): PendingMutation<T> | null {\n // Truth table implementation\n switch (`${existing.type}-${incoming.type}` as const) {\n case `insert-update`: {\n // Update after insert: keep as insert but merge changes\n // For insert-update, the key should remain the same since collections don't allow key changes\n return {\n ...existing,\n type: `insert` as const,\n original: {},\n modified: incoming.modified,\n changes: { ...existing.changes, ...incoming.changes },\n // Keep existing keys (key changes not allowed in updates)\n key: existing.key,\n globalKey: existing.globalKey,\n // Merge metadata (last-write-wins)\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n // Update tracking info\n mutationId: incoming.mutationId,\n updatedAt: incoming.updatedAt,\n }\n }\n\n case `insert-delete`:\n // Delete after insert: cancel both mutations\n return null\n\n case `update-delete`:\n // Delete after update: delete dominates\n return incoming\n\n case `update-update`: {\n // Update after update: replace with latest, union changes\n return {\n ...incoming,\n // Keep original from first update\n original: existing.original,\n // Union the changes from both updates\n changes: { ...existing.changes, ...incoming.changes },\n // Merge metadata\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n }\n }\n\n case `delete-delete`:\n case `insert-insert`:\n // Same type: replace with latest\n return incoming\n\n default: {\n // Exhaustiveness check\n const _exhaustive: never = `${existing.type}-${incoming.type}` as never\n throw new Error(`Unhandled mutation combination: ${_exhaustive}`)\n }\n }\n}\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw new MissingMutationFunctionError()\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingMutateError()\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit().catch(() => {\n // Errors from autoCommit are handled via isPersisted.promise\n // This catch prevents unhandled promise rejections\n })\n }\n\n return this\n }\n\n /**\n * Apply new mutations to this transaction, intelligently merging with existing mutations\n *\n * When mutations operate on the same item (same globalKey), they are merged according to\n * the following rules:\n *\n * - **insert + update** → insert (merge changes, keep empty original)\n * - **insert + delete** → removed (mutations cancel each other out)\n * - **update + delete** → delete (delete dominates)\n * - **update + update** → update (union changes, keep first original)\n * - **same type** → replace with latest\n *\n * This merging reduces over-the-wire churn and keeps the optimistic local view\n * aligned with user intent.\n *\n * @param mutations - Array of new mutations to apply\n */\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n const existingMutation = this.mutations[existingIndex]!\n const mergeResult = mergePendingMutations(existingMutation, newMutation)\n\n if (mergeResult === null) {\n // Remove the mutation (e.g., delete after insert cancels both)\n this.mutations.splice(existingIndex, 1)\n } else {\n // Replace with merged mutation\n this.mutations[existingIndex] = mergeResult\n }\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw new TransactionAlreadyCompletedRollbackError()\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection._state.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection._state.pendingSyncedTransactions.length > 0) {\n mutation.collection._state.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingCommitError()\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n this.isPersisted.resolve(this)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Preserve the original error for rethrowing\n const originalError =\n error instanceof Error ? error : new Error(String(error))\n\n // Update transaction with error information\n this.error = {\n message: originalError.message,\n error: originalError,\n }\n\n // rollback the transaction\n this.rollback()\n\n // Re-throw the original error to preserve identity and stack\n throw originalError\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
@@ -1,9 +1,8 @@
1
1
  import { IStreamBuilder } from '@tanstack/db-ivm';
2
- import { Collection } from './collection.js';
2
+ import { Collection } from './collection/index.js';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
4
  import { Transaction } from './transactions.js';
5
- import { SingleRowRefProxy } from './query/builder/ref-proxy.js';
6
- import { BasicExpression } from './query/ir.js';
5
+ import { BasicExpression, OrderBy } from './query/ir.js';
7
6
  /**
8
7
  * Helper type to extract the output type from a standard schema
9
8
  *
@@ -403,22 +402,25 @@ export type NamespacedAndKeyedStream = IStreamBuilder<KeyedNamespacedRow>;
403
402
  /**
404
403
  * Options for subscribing to collection changes
405
404
  */
406
- export interface SubscribeChangesOptions<T extends object = Record<string, unknown>> {
405
+ export interface SubscribeChangesOptions {
407
406
  /** Whether to include the current state as initial changes */
408
407
  includeInitialState?: boolean;
409
- /** Filter changes using a where expression */
410
- where?: (row: SingleRowRefProxy<T>) => any;
411
408
  /** Pre-compiled expression for filtering changes */
412
409
  whereExpression?: BasicExpression<boolean>;
413
410
  }
411
+ export interface SubscribeChangesSnapshotOptions extends Omit<SubscribeChangesOptions, `includeInitialState`> {
412
+ orderBy?: OrderBy;
413
+ limit?: number;
414
+ }
414
415
  /**
415
416
  * Options for getting current state as changes
416
417
  */
417
- export interface CurrentStateAsChangesOptions<T extends object = Record<string, unknown>> {
418
- /** Filter the current state using a where expression */
419
- where?: (row: SingleRowRefProxy<T>) => any;
418
+ export interface CurrentStateAsChangesOptions {
420
419
  /** Pre-compiled expression for filtering the current state */
421
- whereExpression?: BasicExpression<boolean>;
420
+ where?: BasicExpression<boolean>;
421
+ orderBy?: OrderBy;
422
+ limit?: number;
423
+ optimizedOnly?: boolean;
422
424
  }
423
425
  /**
424
426
  * Function type for listening to collection changes
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.3.1",
4
+ "version": "0.4.0",
5
5
  "dependencies": {
6
6
  "@standard-schema/spec": "^1.0.0",
7
- "@tanstack/db-ivm": "0.1.7"
7
+ "@tanstack/db-ivm": "0.1.8"
8
8
  },
9
9
  "devDependencies": {
10
10
  "@vitest/coverage-istanbul": "^3.2.4",