@tanstack/query-db-collection 0.3.0 → 1.0.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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const query = require("./query.cjs");
4
4
  const errors = require("./errors.cjs");
5
+ const db = require("@tanstack/db");
5
6
  exports.queryCollectionOptions = query.queryCollectionOptions;
6
7
  exports.DeleteOperationItemNotFoundError = errors.DeleteOperationItemNotFoundError;
7
8
  exports.DuplicateKeyInBatchError = errors.DuplicateKeyInBatchError;
@@ -17,4 +18,32 @@ exports.QueryKeyRequiredError = errors.QueryKeyRequiredError;
17
18
  exports.SyncNotInitializedError = errors.SyncNotInitializedError;
18
19
  exports.UnknownOperationTypeError = errors.UnknownOperationTypeError;
19
20
  exports.UpdateOperationItemNotFoundError = errors.UpdateOperationItemNotFoundError;
21
+ Object.defineProperty(exports, "extractFieldPath", {
22
+ enumerable: true,
23
+ get: () => db.extractFieldPath
24
+ });
25
+ Object.defineProperty(exports, "extractSimpleComparisons", {
26
+ enumerable: true,
27
+ get: () => db.extractSimpleComparisons
28
+ });
29
+ Object.defineProperty(exports, "extractValue", {
30
+ enumerable: true,
31
+ get: () => db.extractValue
32
+ });
33
+ Object.defineProperty(exports, "parseLoadSubsetOptions", {
34
+ enumerable: true,
35
+ get: () => db.parseLoadSubsetOptions
36
+ });
37
+ Object.defineProperty(exports, "parseOrderByExpression", {
38
+ enumerable: true,
39
+ get: () => db.parseOrderByExpression
40
+ });
41
+ Object.defineProperty(exports, "parseWhereExpression", {
42
+ enumerable: true,
43
+ get: () => db.parseWhereExpression
44
+ });
45
+ Object.defineProperty(exports, "walkExpression", {
46
+ enumerable: true,
47
+ get: () => db.walkExpression
48
+ });
20
49
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,2 +1,3 @@
1
1
  export { queryCollectionOptions, type QueryCollectionConfig, type QueryCollectionUtils, type SyncOperation, } from './query.cjs';
2
2
  export * from './errors.cjs';
3
+ export { parseWhereExpression, parseOrderByExpression, extractSimpleComparisons, parseLoadSubsetOptions, extractFieldPath, extractValue, walkExpression, type FieldPath, type SimpleComparison, type ParseWhereOptions, type ParsedOrderBy, } from '@tanstack/db';
@@ -32,19 +32,32 @@ class QueryCollectionUtilsImpl {
32
32
  }
33
33
  // Getters for QueryObserver state
34
34
  get isFetching() {
35
- return this.state.queryObserver?.getCurrentResult().isFetching ?? false;
35
+ return Array.from(this.state.observers.values()).some(
36
+ (observer) => observer.getCurrentResult().isFetching
37
+ );
36
38
  }
37
39
  get isRefetching() {
38
- return this.state.queryObserver?.getCurrentResult().isRefetching ?? false;
40
+ return Array.from(this.state.observers.values()).some(
41
+ (observer) => observer.getCurrentResult().isRefetching
42
+ );
39
43
  }
40
44
  get isLoading() {
41
- return this.state.queryObserver?.getCurrentResult().isLoading ?? false;
45
+ return Array.from(this.state.observers.values()).some(
46
+ (observer) => observer.getCurrentResult().isLoading
47
+ );
42
48
  }
43
49
  get dataUpdatedAt() {
44
- return this.state.queryObserver?.getCurrentResult().dataUpdatedAt ?? 0;
50
+ return Math.max(
51
+ 0,
52
+ ...Array.from(this.state.observers.values()).map(
53
+ (observer) => observer.getCurrentResult().dataUpdatedAt
54
+ )
55
+ );
45
56
  }
46
57
  get fetchStatus() {
47
- return this.state.queryObserver?.getCurrentResult().fetchStatus ?? `idle`;
58
+ return Array.from(this.state.observers.values()).map(
59
+ (observer) => observer.getCurrentResult().fetchStatus
60
+ );
48
61
  }
49
62
  }
50
63
  function queryCollectionOptions(config) {
@@ -65,6 +78,7 @@ function queryCollectionOptions(config) {
65
78
  meta,
66
79
  ...baseCollectionConfig
67
80
  } = config;
81
+ const syncMode = baseCollectionConfig.syncMode ?? `eager`;
68
82
  if (!queryKey) {
69
83
  throw new errors.QueryKeyRequiredError();
70
84
  }
@@ -81,126 +95,266 @@ function queryCollectionOptions(config) {
81
95
  lastError: void 0,
82
96
  errorCount: 0,
83
97
  lastErrorUpdatedAt: 0,
84
- queryObserver: void 0
98
+ observers: /* @__PURE__ */ new Map()
99
+ };
100
+ const hashToQueryKey = /* @__PURE__ */ new Map();
101
+ const queryToRows = /* @__PURE__ */ new Map();
102
+ const rowToQueries = /* @__PURE__ */ new Map();
103
+ const unsubscribes = /* @__PURE__ */ new Map();
104
+ const addRow = (rowKey, hashedQueryKey) => {
105
+ const rowToQueriesSet = rowToQueries.get(rowKey) || /* @__PURE__ */ new Set();
106
+ rowToQueriesSet.add(hashedQueryKey);
107
+ rowToQueries.set(rowKey, rowToQueriesSet);
108
+ const queryToRowsSet = queryToRows.get(hashedQueryKey) || /* @__PURE__ */ new Set();
109
+ queryToRowsSet.add(rowKey);
110
+ queryToRows.set(hashedQueryKey, queryToRowsSet);
111
+ };
112
+ const removeRow = (rowKey, hashedQuerKey) => {
113
+ const rowToQueriesSet = rowToQueries.get(rowKey) || /* @__PURE__ */ new Set();
114
+ rowToQueriesSet.delete(hashedQuerKey);
115
+ rowToQueries.set(rowKey, rowToQueriesSet);
116
+ const queryToRowsSet = queryToRows.get(hashedQuerKey) || /* @__PURE__ */ new Set();
117
+ queryToRowsSet.delete(rowKey);
118
+ queryToRows.set(hashedQuerKey, queryToRowsSet);
119
+ return rowToQueriesSet.size === 0;
85
120
  };
86
121
  const internalSync = (params) => {
87
122
  const { begin, write, commit, markReady, collection } = params;
88
- const observerOptions = {
89
- queryKey,
90
- queryFn,
91
- structuralSharing: true,
92
- notifyOnChangeProps: `all`,
93
- // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used
94
- ...meta !== void 0 && { meta },
95
- ...enabled !== void 0 && { enabled },
96
- ...refetchInterval !== void 0 && { refetchInterval },
97
- ...retry !== void 0 && { retry },
98
- ...retryDelay !== void 0 && { retryDelay },
99
- ...staleTime !== void 0 && { staleTime }
100
- };
101
- const localObserver = new queryCore.QueryObserver(queryClient, observerOptions);
102
- state.queryObserver = localObserver;
103
- let isSubscribed = false;
104
- let actualUnsubscribeFn = null;
105
- const handleQueryResult = (result) => {
106
- if (result.isSuccess) {
107
- state.lastError = void 0;
108
- state.errorCount = 0;
109
- const rawData = result.data;
110
- const newItemsArray = select ? select(rawData) : rawData;
111
- if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
112
- const errorMessage = select ? `@tanstack/query-db-collection: select() must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}` : `@tanstack/query-db-collection: queryFn must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}`;
113
- console.error(errorMessage);
114
- return;
115
- }
116
- const currentSyncedItems = new Map(
117
- collection._state.syncedData.entries()
118
- );
119
- const newItemsMap = /* @__PURE__ */ new Map();
120
- newItemsArray.forEach((item) => {
121
- const key = getKey(item);
122
- newItemsMap.set(key, item);
123
- });
124
- begin();
125
- const shallowEqual = (obj1, obj2) => {
126
- const keys1 = Object.keys(obj1);
127
- const keys2 = Object.keys(obj2);
128
- if (keys1.length !== keys2.length) return false;
129
- return keys1.every((key) => {
130
- if (typeof obj1[key] === `function`) return true;
131
- return obj1[key] === obj2[key];
123
+ let syncStarted = false;
124
+ const createQueryFromOpts = (opts = {}, queryFunction = queryFn) => {
125
+ const key = typeof queryKey === `function` ? queryKey(opts) : queryKey;
126
+ const hashedQueryKey = queryCore.hashKey(key);
127
+ const extendedMeta = { ...meta, loadSubsetOptions: opts };
128
+ if (state.observers.has(hashedQueryKey)) {
129
+ const observer = state.observers.get(hashedQueryKey);
130
+ const currentResult = observer.getCurrentResult();
131
+ if (currentResult.isSuccess) {
132
+ return true;
133
+ } else if (currentResult.isError) {
134
+ return Promise.reject(currentResult.error);
135
+ } else {
136
+ return new Promise((resolve, reject) => {
137
+ const unsubscribe = observer.subscribe((result) => {
138
+ if (result.isSuccess) {
139
+ unsubscribe();
140
+ resolve();
141
+ } else if (result.isError) {
142
+ unsubscribe();
143
+ reject(result.error);
144
+ }
145
+ });
132
146
  });
133
- };
134
- currentSyncedItems.forEach((oldItem, key) => {
135
- const newItem = newItemsMap.get(key);
136
- if (!newItem) {
137
- write({ type: `delete`, value: oldItem });
138
- } else if (!shallowEqual(
139
- oldItem,
140
- newItem
141
- )) {
142
- write({ type: `update`, value: newItem });
147
+ }
148
+ }
149
+ const observerOptions = {
150
+ queryKey: key,
151
+ queryFn: queryFunction,
152
+ meta: extendedMeta,
153
+ structuralSharing: true,
154
+ notifyOnChangeProps: `all`,
155
+ // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used
156
+ ...enabled !== void 0 && { enabled },
157
+ ...refetchInterval !== void 0 && { refetchInterval },
158
+ ...retry !== void 0 && { retry },
159
+ ...retryDelay !== void 0 && { retryDelay },
160
+ ...staleTime !== void 0 && { staleTime }
161
+ };
162
+ const localObserver = new queryCore.QueryObserver(queryClient, observerOptions);
163
+ hashToQueryKey.set(hashedQueryKey, key);
164
+ state.observers.set(hashedQueryKey, localObserver);
165
+ const readyPromise = new Promise((resolve, reject) => {
166
+ const unsubscribe = localObserver.subscribe((result) => {
167
+ if (result.isSuccess) {
168
+ unsubscribe();
169
+ resolve();
170
+ } else if (result.isError) {
171
+ unsubscribe();
172
+ reject(result.error);
143
173
  }
144
174
  });
145
- newItemsMap.forEach((newItem, key) => {
146
- if (!currentSyncedItems.has(key)) {
147
- write({ type: `insert`, value: newItem });
175
+ });
176
+ if (syncStarted || collection.subscriberCount > 0) {
177
+ subscribeToQuery(localObserver, hashedQueryKey);
178
+ }
179
+ const subscription = opts.subscription;
180
+ subscription?.once(`unsubscribed`, () => {
181
+ queryClient.removeQueries({ queryKey: key, exact: true });
182
+ });
183
+ return readyPromise;
184
+ };
185
+ const makeQueryResultHandler = (queryKey2) => {
186
+ const hashedQueryKey = queryCore.hashKey(queryKey2);
187
+ const handleQueryResult = (result) => {
188
+ if (result.isSuccess) {
189
+ state.lastError = void 0;
190
+ state.errorCount = 0;
191
+ const rawData = result.data;
192
+ const newItemsArray = select ? select(rawData) : rawData;
193
+ if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
194
+ const errorMessage = select ? `@tanstack/query-db-collection: select() must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey2)}` : `@tanstack/query-db-collection: queryFn must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey2)}`;
195
+ console.error(errorMessage);
196
+ return;
148
197
  }
149
- });
150
- commit();
151
- markReady();
152
- } else if (result.isError) {
153
- if (result.errorUpdatedAt !== state.lastErrorUpdatedAt) {
154
- state.lastError = result.error;
155
- state.errorCount++;
156
- state.lastErrorUpdatedAt = result.errorUpdatedAt;
198
+ const currentSyncedItems = new Map(
199
+ collection._state.syncedData.entries()
200
+ );
201
+ const newItemsMap = /* @__PURE__ */ new Map();
202
+ newItemsArray.forEach((item) => {
203
+ const key = getKey(item);
204
+ newItemsMap.set(key, item);
205
+ });
206
+ begin();
207
+ const shallowEqual = (obj1, obj2) => {
208
+ const keys1 = Object.keys(obj1);
209
+ const keys2 = Object.keys(obj2);
210
+ if (keys1.length !== keys2.length) return false;
211
+ return keys1.every((key) => {
212
+ if (typeof obj1[key] === `function`) return true;
213
+ return obj1[key] === obj2[key];
214
+ });
215
+ };
216
+ currentSyncedItems.forEach((oldItem, key) => {
217
+ const newItem = newItemsMap.get(key);
218
+ if (!newItem) {
219
+ const needToRemove = removeRow(key, hashedQueryKey);
220
+ if (needToRemove) {
221
+ write({ type: `delete`, value: oldItem });
222
+ }
223
+ } else if (!shallowEqual(
224
+ oldItem,
225
+ newItem
226
+ )) {
227
+ write({ type: `update`, value: newItem });
228
+ }
229
+ });
230
+ newItemsMap.forEach((newItem, key) => {
231
+ addRow(key, hashedQueryKey);
232
+ if (!currentSyncedItems.has(key)) {
233
+ write({ type: `insert`, value: newItem });
234
+ }
235
+ });
236
+ commit();
237
+ markReady();
238
+ } else if (result.isError) {
239
+ if (result.errorUpdatedAt !== state.lastErrorUpdatedAt) {
240
+ state.lastError = result.error;
241
+ state.errorCount++;
242
+ state.lastErrorUpdatedAt = result.errorUpdatedAt;
243
+ }
244
+ console.error(
245
+ `[QueryCollection] Error observing query ${String(queryKey2)}:`,
246
+ result.error
247
+ );
248
+ markReady();
157
249
  }
158
- console.error(
159
- `[QueryCollection] Error observing query ${String(queryKey)}:`,
160
- result.error
161
- );
162
- markReady();
163
- }
250
+ };
251
+ return handleQueryResult;
164
252
  };
165
- const subscribeToQuery = () => {
166
- if (!isSubscribed) {
167
- actualUnsubscribeFn = localObserver.subscribe(handleQueryResult);
168
- isSubscribed = true;
169
- }
253
+ const isSubscribed = (hashedQueryKey) => {
254
+ return unsubscribes.has(hashedQueryKey);
170
255
  };
171
- const unsubscribeFromQuery = () => {
172
- if (isSubscribed && actualUnsubscribeFn) {
173
- actualUnsubscribeFn();
174
- actualUnsubscribeFn = null;
175
- isSubscribed = false;
256
+ const subscribeToQuery = (observer, hashedQueryKey) => {
257
+ if (!isSubscribed(hashedQueryKey)) {
258
+ const queryKey2 = hashToQueryKey.get(hashedQueryKey);
259
+ const handleQueryResult = makeQueryResultHandler(queryKey2);
260
+ const unsubscribeFn = observer.subscribe(handleQueryResult);
261
+ unsubscribes.set(hashedQueryKey, unsubscribeFn);
176
262
  }
177
263
  };
178
- subscribeToQuery();
264
+ const subscribeToQueries = () => {
265
+ state.observers.forEach(subscribeToQuery);
266
+ };
267
+ const unsubscribeFromQueries = () => {
268
+ unsubscribes.forEach((unsubscribeFn) => {
269
+ unsubscribeFn();
270
+ });
271
+ unsubscribes.clear();
272
+ };
273
+ syncStarted = true;
179
274
  const unsubscribeFromCollectionEvents = collection.on(
180
275
  `subscribers:change`,
181
276
  ({ subscriberCount }) => {
182
277
  if (subscriberCount > 0) {
183
- subscribeToQuery();
278
+ subscribeToQueries();
184
279
  } else if (subscriberCount === 0) {
185
- unsubscribeFromQuery();
280
+ unsubscribeFromQueries();
186
281
  }
187
282
  }
188
283
  );
189
- handleQueryResult(localObserver.getCurrentResult());
190
- return async () => {
284
+ if (syncMode === `eager`) {
285
+ const initialResult = createQueryFromOpts({});
286
+ if (initialResult instanceof Promise) {
287
+ initialResult.catch(() => {
288
+ });
289
+ }
290
+ } else {
291
+ markReady();
292
+ }
293
+ subscribeToQueries();
294
+ state.observers.forEach((observer, hashedQueryKey) => {
295
+ const queryKey2 = hashToQueryKey.get(hashedQueryKey);
296
+ const handleQueryResult = makeQueryResultHandler(queryKey2);
297
+ handleQueryResult(observer.getCurrentResult());
298
+ });
299
+ const unsubscribeQueryCache = queryClient.getQueryCache().subscribe((event) => {
300
+ const hashedKey = event.query.queryHash;
301
+ if (event.type === `removed`) {
302
+ cleanupQuery(hashedKey);
303
+ }
304
+ });
305
+ function cleanupQuery(hashedQueryKey) {
306
+ unsubscribes.get(hashedQueryKey)?.();
307
+ const rowKeys = queryToRows.get(hashedQueryKey) ?? /* @__PURE__ */ new Set();
308
+ rowKeys.forEach((rowKey) => {
309
+ const queries = rowToQueries.get(rowKey);
310
+ if (queries && queries.size > 0) {
311
+ queries.delete(hashedQueryKey);
312
+ if (queries.size === 0) {
313
+ rowToQueries.delete(rowKey);
314
+ if (collection.has(rowKey)) {
315
+ begin();
316
+ write({ type: `delete`, value: collection.get(rowKey) });
317
+ commit();
318
+ }
319
+ }
320
+ }
321
+ });
322
+ unsubscribes.delete(hashedQueryKey);
323
+ state.observers.delete(hashedQueryKey);
324
+ queryToRows.delete(hashedQueryKey);
325
+ hashToQueryKey.delete(hashedQueryKey);
326
+ }
327
+ const cleanup = async () => {
191
328
  unsubscribeFromCollectionEvents();
192
- unsubscribeFromQuery();
193
- await queryClient.cancelQueries({ queryKey });
194
- queryClient.removeQueries({ queryKey });
329
+ unsubscribeFromQueries();
330
+ const queryKeys = [...hashToQueryKey.values()];
331
+ hashToQueryKey.clear();
332
+ queryToRows.clear();
333
+ rowToQueries.clear();
334
+ state.observers.clear();
335
+ unsubscribeQueryCache();
336
+ await Promise.all(
337
+ queryKeys.map(async (queryKey2) => {
338
+ await queryClient.cancelQueries({ queryKey: queryKey2 });
339
+ queryClient.removeQueries({ queryKey: queryKey2 });
340
+ })
341
+ );
342
+ };
343
+ const loadSubsetDedupe = syncMode === `eager` ? void 0 : createQueryFromOpts;
344
+ return {
345
+ loadSubset: loadSubsetDedupe,
346
+ cleanup
195
347
  };
196
348
  };
197
349
  const refetch = async (opts) => {
198
- if (!state.queryObserver) {
199
- return;
200
- }
201
- return state.queryObserver.refetch({
202
- throwOnError: opts?.throwOnError
350
+ const queryKeys = [...hashToQueryKey.values()];
351
+ const refetchPromises = queryKeys.map((queryKey2) => {
352
+ const queryObserver = state.observers.get(queryCore.hashKey(queryKey2));
353
+ return queryObserver.refetch({
354
+ throwOnError: opts?.throwOnError
355
+ });
203
356
  });
357
+ await Promise.all(refetchPromises);
204
358
  };
205
359
  let writeContext = null;
206
360
  const enhancedInternalSync = (params) => {
@@ -247,6 +401,7 @@ function queryCollectionOptions(config) {
247
401
  return {
248
402
  ...baseCollectionConfig,
249
403
  getKey,
404
+ syncMode,
250
405
  sync: { sync: enhancedInternalSync },
251
406
  onInsert: wrappedOnInsert,
252
407
  onUpdate: wrappedOnUpdate,