@tanstack/db 0.4.7 → 0.4.9

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 (115) hide show
  1. package/dist/cjs/collection/index.cjs.map +1 -1
  2. package/dist/cjs/collection/index.d.cts +2 -1
  3. package/dist/cjs/collection/lifecycle.cjs +2 -3
  4. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  5. package/dist/cjs/collection/state.cjs +22 -33
  6. package/dist/cjs/collection/state.cjs.map +1 -1
  7. package/dist/cjs/collection/state.d.cts +6 -2
  8. package/dist/cjs/collection/sync.cjs +4 -3
  9. package/dist/cjs/collection/sync.cjs.map +1 -1
  10. package/dist/cjs/errors.cjs +51 -17
  11. package/dist/cjs/errors.cjs.map +1 -1
  12. package/dist/cjs/errors.d.cts +38 -8
  13. package/dist/cjs/index.cjs +8 -4
  14. package/dist/cjs/index.cjs.map +1 -1
  15. package/dist/cjs/indexes/auto-index.cjs +0 -3
  16. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  17. package/dist/cjs/query/builder/types.d.cts +1 -1
  18. package/dist/cjs/query/compiler/index.cjs +42 -19
  19. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  20. package/dist/cjs/query/compiler/index.d.cts +33 -8
  21. package/dist/cjs/query/compiler/joins.cjs +88 -66
  22. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  23. package/dist/cjs/query/compiler/joins.d.cts +5 -2
  24. package/dist/cjs/query/compiler/order-by.cjs +2 -0
  25. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  26. package/dist/cjs/query/compiler/order-by.d.cts +1 -0
  27. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  28. package/dist/cjs/query/live/collection-config-builder.cjs +322 -46
  29. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  30. package/dist/cjs/query/live/collection-config-builder.d.cts +98 -7
  31. package/dist/cjs/query/live/collection-registry.cjs +16 -0
  32. package/dist/cjs/query/live/collection-registry.cjs.map +1 -0
  33. package/dist/cjs/query/live/collection-registry.d.cts +26 -0
  34. package/dist/cjs/query/live/collection-subscriber.cjs +57 -58
  35. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  36. package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
  37. package/dist/cjs/query/live-query-collection.cjs +11 -5
  38. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  39. package/dist/cjs/query/live-query-collection.d.cts +10 -3
  40. package/dist/cjs/query/optimizer.cjs +44 -7
  41. package/dist/cjs/query/optimizer.cjs.map +1 -1
  42. package/dist/cjs/query/optimizer.d.cts +4 -4
  43. package/dist/cjs/scheduler.cjs +137 -0
  44. package/dist/cjs/scheduler.cjs.map +1 -0
  45. package/dist/cjs/scheduler.d.cts +56 -0
  46. package/dist/cjs/transactions.cjs +7 -1
  47. package/dist/cjs/transactions.cjs.map +1 -1
  48. package/dist/cjs/types.d.cts +3 -5
  49. package/dist/esm/collection/index.d.ts +2 -1
  50. package/dist/esm/collection/index.js.map +1 -1
  51. package/dist/esm/collection/lifecycle.js +2 -3
  52. package/dist/esm/collection/lifecycle.js.map +1 -1
  53. package/dist/esm/collection/state.d.ts +6 -2
  54. package/dist/esm/collection/state.js +22 -33
  55. package/dist/esm/collection/state.js.map +1 -1
  56. package/dist/esm/collection/sync.js +4 -3
  57. package/dist/esm/collection/sync.js.map +1 -1
  58. package/dist/esm/errors.d.ts +38 -8
  59. package/dist/esm/errors.js +52 -18
  60. package/dist/esm/errors.js.map +1 -1
  61. package/dist/esm/index.js +9 -5
  62. package/dist/esm/indexes/auto-index.js +0 -3
  63. package/dist/esm/indexes/auto-index.js.map +1 -1
  64. package/dist/esm/query/builder/types.d.ts +1 -1
  65. package/dist/esm/query/compiler/index.d.ts +33 -8
  66. package/dist/esm/query/compiler/index.js +42 -19
  67. package/dist/esm/query/compiler/index.js.map +1 -1
  68. package/dist/esm/query/compiler/joins.d.ts +5 -2
  69. package/dist/esm/query/compiler/joins.js +90 -68
  70. package/dist/esm/query/compiler/joins.js.map +1 -1
  71. package/dist/esm/query/compiler/order-by.d.ts +1 -0
  72. package/dist/esm/query/compiler/order-by.js +2 -0
  73. package/dist/esm/query/compiler/order-by.js.map +1 -1
  74. package/dist/esm/query/compiler/select.js.map +1 -1
  75. package/dist/esm/query/live/collection-config-builder.d.ts +98 -7
  76. package/dist/esm/query/live/collection-config-builder.js +322 -46
  77. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  78. package/dist/esm/query/live/collection-registry.d.ts +26 -0
  79. package/dist/esm/query/live/collection-registry.js +16 -0
  80. package/dist/esm/query/live/collection-registry.js.map +1 -0
  81. package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
  82. package/dist/esm/query/live/collection-subscriber.js +57 -58
  83. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  84. package/dist/esm/query/live-query-collection.d.ts +10 -3
  85. package/dist/esm/query/live-query-collection.js +11 -5
  86. package/dist/esm/query/live-query-collection.js.map +1 -1
  87. package/dist/esm/query/optimizer.d.ts +4 -4
  88. package/dist/esm/query/optimizer.js +44 -7
  89. package/dist/esm/query/optimizer.js.map +1 -1
  90. package/dist/esm/scheduler.d.ts +56 -0
  91. package/dist/esm/scheduler.js +137 -0
  92. package/dist/esm/scheduler.js.map +1 -0
  93. package/dist/esm/transactions.js +7 -1
  94. package/dist/esm/transactions.js.map +1 -1
  95. package/dist/esm/types.d.ts +3 -5
  96. package/package.json +2 -2
  97. package/src/collection/index.ts +1 -1
  98. package/src/collection/lifecycle.ts +3 -4
  99. package/src/collection/state.ts +52 -48
  100. package/src/collection/sync.ts +7 -6
  101. package/src/errors.ts +79 -13
  102. package/src/indexes/auto-index.ts +0 -8
  103. package/src/query/builder/types.ts +1 -1
  104. package/src/query/compiler/index.ts +115 -32
  105. package/src/query/compiler/joins.ts +180 -127
  106. package/src/query/compiler/order-by.ts +7 -0
  107. package/src/query/compiler/select.ts +2 -3
  108. package/src/query/live/collection-config-builder.ts +542 -71
  109. package/src/query/live/collection-registry.ts +47 -0
  110. package/src/query/live/collection-subscriber.ts +87 -105
  111. package/src/query/live-query-collection.ts +39 -14
  112. package/src/query/optimizer.ts +85 -15
  113. package/src/scheduler.ts +198 -0
  114. package/src/transactions.ts +12 -1
  115. package/src/types.ts +3 -5
@@ -1,37 +1,128 @@
1
+ import { SchedulerContextId } from '../../scheduler.js';
1
2
  import { CollectionSubscription } from '../../collection/subscription.js';
2
3
  import { OrderByOptimizationInfo } from '../compiler/order-by.js';
3
- import { CollectionConfigSingleRowOption, SyncConfig } from '../../types.js';
4
+ import { CollectionConfigSingleRowOption, SyncConfig, UtilsRecord } from '../../types.js';
4
5
  import { Context, GetResult } from '../builder/types.js';
5
6
  import { BasicExpression, QueryIR } from '../ir.js';
6
7
  import { LazyCollectionCallbacks } from '../compiler/joins.js';
7
8
  import { FullSyncState, LiveQueryCollectionConfig } from './types.js';
9
+ export type LiveQueryCollectionUtils = UtilsRecord & {
10
+ getRunCount: () => number;
11
+ getBuilder: () => CollectionConfigBuilder<any, any>;
12
+ };
8
13
  export declare class CollectionConfigBuilder<TContext extends Context, TResult extends object = GetResult<TContext>> {
9
14
  private readonly config;
10
15
  private readonly id;
11
16
  readonly query: QueryIR;
12
17
  private readonly collections;
18
+ private readonly collectionByAlias;
19
+ private compiledAliasToCollectionId;
13
20
  private readonly resultKeys;
14
21
  private readonly orderByIndices;
15
22
  private readonly compare?;
16
23
  private isGraphRunning;
24
+ private runCount;
25
+ currentSyncConfig: Parameters<SyncConfig<TResult>[`sync`]>[0] | undefined;
26
+ currentSyncState: FullSyncState | undefined;
27
+ private isInErrorState;
28
+ private liveQueryCollection?;
29
+ private readonly aliasDependencies;
30
+ private readonly builderDependencies;
31
+ private readonly pendingGraphRuns;
32
+ private unsubscribeFromSchedulerClears?;
17
33
  private graphCache;
18
34
  private inputsCache;
19
35
  private pipelineCache;
20
- collectionWhereClausesCache: Map<string, BasicExpression<boolean>> | undefined;
36
+ sourceWhereClausesCache: Map<string, BasicExpression<boolean>> | undefined;
21
37
  readonly subscriptions: Record<string, CollectionSubscription>;
22
- lazyCollectionsCallbacks: Record<string, LazyCollectionCallbacks>;
23
- readonly lazyCollections: Set<string>;
38
+ lazySourcesCallbacks: Record<string, LazyCollectionCallbacks>;
39
+ readonly lazySources: Set<string>;
24
40
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>;
25
41
  constructor(config: LiveQueryCollectionConfig<TContext, TResult>);
26
- getConfig(): CollectionConfigSingleRowOption<TResult>;
27
- maybeRunGraph(config: Parameters<SyncConfig<TResult>[`sync`]>[0], syncState: FullSyncState, callback?: () => boolean): void;
42
+ getConfig(): CollectionConfigSingleRowOption<TResult> & {
43
+ utils: LiveQueryCollectionUtils;
44
+ };
45
+ /**
46
+ * Resolves a collection alias to its collection ID.
47
+ *
48
+ * Uses a two-tier lookup strategy:
49
+ * 1. First checks compiled aliases (includes subquery inner aliases)
50
+ * 2. Falls back to declared aliases from the query's from/join clauses
51
+ *
52
+ * @param alias - The alias to resolve (e.g., "employee", "manager")
53
+ * @returns The collection ID that the alias references
54
+ * @throws {Error} If the alias is not found in either lookup
55
+ */
56
+ getCollectionIdForAlias(alias: string): string;
57
+ isLazyAlias(alias: string): boolean;
58
+ maybeRunGraph(callback?: () => boolean): void;
59
+ /**
60
+ * Schedules a graph run with the transaction-scoped scheduler.
61
+ * Ensures each builder runs at most once per transaction, with automatic dependency tracking
62
+ * to run parent queries before child queries. Outside a transaction, runs immediately.
63
+ *
64
+ * Multiple calls during a transaction are coalesced into a single execution.
65
+ * Dependencies are auto-discovered from subscribed live queries, or can be overridden.
66
+ * Load callbacks are combined when entries merge.
67
+ *
68
+ * Uses the current sync session's config and syncState from instance properties.
69
+ *
70
+ * @param callback - Optional callback to load more data if needed (returns true when done)
71
+ * @param options - Optional scheduling configuration
72
+ * @param options.contextId - Transaction ID to group work; defaults to active transaction
73
+ * @param options.jobId - Unique identifier for this job; defaults to this builder instance
74
+ * @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies
75
+ * @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies
76
+ */
77
+ scheduleGraphRun(callback?: () => boolean, options?: {
78
+ contextId?: SchedulerContextId;
79
+ jobId?: unknown;
80
+ alias?: string;
81
+ dependencies?: Array<CollectionConfigBuilder<any, any>>;
82
+ }): void;
83
+ /**
84
+ * Clears pending graph run state for a specific context.
85
+ * Called when the scheduler clears a context (e.g., transaction rollback/abort).
86
+ */
87
+ clearPendingGraphRun(contextId: SchedulerContextId): void;
88
+ /**
89
+ * Executes a pending graph run. Called by the scheduler when dependencies are satisfied.
90
+ * Clears the pending state BEFORE execution so that any re-schedules during the run
91
+ * create fresh state and don't interfere with the current execution.
92
+ * Uses instance sync state - if sync has ended, gracefully returns without executing.
93
+ *
94
+ * @param contextId - Optional context ID to look up pending state
95
+ * @param pendingParam - For immediate execution (no context), pending state is passed directly
96
+ */
97
+ private executeGraphRun;
28
98
  private getSyncConfig;
99
+ incrementRunCount(): void;
100
+ getRunCount(): number;
29
101
  private syncFn;
102
+ /**
103
+ * Compiles the query pipeline with all declared aliases.
104
+ */
30
105
  private compileBasePipeline;
31
106
  private maybeCompileBasePipeline;
32
107
  private extendPipelineWithChangeProcessing;
33
108
  private applyChanges;
109
+ /**
110
+ * Handle status changes from source collections
111
+ */
112
+ private handleSourceStatusChange;
113
+ /**
114
+ * Update the live query status based on source collection statuses
115
+ */
116
+ private updateLiveQueryStatus;
117
+ /**
118
+ * Transition the live query to error state
119
+ */
120
+ private transitionToError;
34
121
  private allCollectionsReady;
35
- private allCollectionsReadyOrInitialCommit;
122
+ /**
123
+ * Creates per-alias subscriptions enabling self-join support.
124
+ * Each alias gets its own subscription with independent filters, even for the same collection.
125
+ * Example: `{ employee: col, manager: col }` creates two separate subscriptions.
126
+ */
36
127
  private subscribeToAllCollections;
37
128
  }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const collectionBuilderRegistry = /* @__PURE__ */ new WeakMap();
4
+ function getBuilderFromConfig(config) {
5
+ return config.utils?.getBuilder?.();
6
+ }
7
+ function registerCollectionBuilder(collection, builder) {
8
+ collectionBuilderRegistry.set(collection, builder);
9
+ }
10
+ function getCollectionBuilder(collection) {
11
+ return collectionBuilderRegistry.get(collection);
12
+ }
13
+ exports.getBuilderFromConfig = getBuilderFromConfig;
14
+ exports.getCollectionBuilder = getCollectionBuilder;
15
+ exports.registerCollectionBuilder = registerCollectionBuilder;
16
+ //# sourceMappingURL=collection-registry.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection-registry.cjs","sources":["../../../../src/query/live/collection-registry.ts"],"sourcesContent":["import type { Collection } from \"../../collection/index.js\"\nimport type { CollectionConfigBuilder } from \"./collection-config-builder.js\"\n\nconst collectionBuilderRegistry = new WeakMap<\n Collection<any, any, any>,\n CollectionConfigBuilder<any, any>\n>()\n\n/**\n * Retrieves the builder attached to a config object via its utils.getBuilder() method.\n *\n * @param config - The collection config object\n * @returns The attached builder, or `undefined` if none exists\n */\nexport function getBuilderFromConfig(\n config: object\n): CollectionConfigBuilder<any, any> | undefined {\n return (config as any).utils?.getBuilder?.()\n}\n\n/**\n * Registers a builder for a collection in the global registry.\n * Used to detect when a live query depends on another live query,\n * enabling the scheduler to ensure parent queries run first.\n *\n * @param collection - The collection to register the builder for\n * @param builder - The builder that produces this collection\n */\nexport function registerCollectionBuilder(\n collection: Collection<any, any, any>,\n builder: CollectionConfigBuilder<any, any>\n): void {\n collectionBuilderRegistry.set(collection, builder)\n}\n\n/**\n * Retrieves the builder registered for a collection.\n * Used to discover dependencies when a live query subscribes to another live query.\n *\n * @param collection - The collection to look up\n * @returns The registered builder, or `undefined` if none exists\n */\nexport function getCollectionBuilder(\n collection: Collection<any, any, any>\n): CollectionConfigBuilder<any, any> | undefined {\n return collectionBuilderRegistry.get(collection)\n}\n"],"names":[],"mappings":";;AAGA,MAAM,gDAAgC,QAAA;AAW/B,SAAS,qBACd,QAC+C;AAC/C,SAAQ,OAAe,OAAO,aAAA;AAChC;AAUO,SAAS,0BACd,YACA,SACM;AACN,4BAA0B,IAAI,YAAY,OAAO;AACnD;AASO,SAAS,qBACd,YAC+C;AAC/C,SAAO,0BAA0B,IAAI,UAAU;AACjD;;;;"}
@@ -0,0 +1,26 @@
1
+ import { Collection } from '../../collection/index.js';
2
+ import { CollectionConfigBuilder } from './collection-config-builder.js';
3
+ /**
4
+ * Retrieves the builder attached to a config object via its utils.getBuilder() method.
5
+ *
6
+ * @param config - The collection config object
7
+ * @returns The attached builder, or `undefined` if none exists
8
+ */
9
+ export declare function getBuilderFromConfig(config: object): CollectionConfigBuilder<any, any> | undefined;
10
+ /**
11
+ * Registers a builder for a collection in the global registry.
12
+ * Used to detect when a live query depends on another live query,
13
+ * enabling the scheduler to ensure parent queries run first.
14
+ *
15
+ * @param collection - The collection to register the builder for
16
+ * @param builder - The builder that produces this collection
17
+ */
18
+ export declare function registerCollectionBuilder(collection: Collection<any, any, any>, builder: CollectionConfigBuilder<any, any>): void;
19
+ /**
20
+ * Retrieves the builder registered for a collection.
21
+ * Used to discover dependencies when a live query subscribes to another live query.
22
+ *
23
+ * @param collection - The collection to look up
24
+ * @returns The registered builder, or `undefined` if none exists
25
+ */
26
+ export declare function getCollectionBuilder(collection: Collection<any, any, any>): CollectionConfigBuilder<any, any> | undefined;
@@ -2,46 +2,41 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const dbIvm = require("@tanstack/db-ivm");
4
4
  const expressions = require("../compiler/expressions.cjs");
5
+ const errors = require("../../errors.cjs");
6
+ const loadMoreCallbackSymbol = Symbol.for(
7
+ `@tanstack/db.collection-config-builder`
8
+ );
5
9
  class CollectionSubscriber {
6
- constructor(collectionId, collection, config, syncState, collectionConfigBuilder) {
10
+ constructor(alias, collectionId, collection, collectionConfigBuilder) {
11
+ this.alias = alias;
7
12
  this.collectionId = collectionId;
8
13
  this.collection = collection;
9
- this.config = config;
10
- this.syncState = syncState;
11
14
  this.collectionConfigBuilder = collectionConfigBuilder;
12
15
  this.biggest = void 0;
13
- this.collectionAlias = findCollectionAlias(
14
- this.collectionId,
15
- this.collectionConfigBuilder.query
16
- );
17
16
  }
18
17
  subscribe() {
19
- const whereClause = this.getWhereClauseFromAlias(this.collectionAlias);
18
+ const whereClause = this.getWhereClauseForAlias();
20
19
  if (whereClause) {
21
- const whereExpression = expressions.convertToBasicExpression(
22
- whereClause,
23
- this.collectionAlias
24
- );
20
+ const whereExpression = expressions.convertToBasicExpression(whereClause, this.alias);
25
21
  if (whereExpression) {
26
22
  return this.subscribeToChanges(whereExpression);
27
- } else {
28
- throw new Error(
29
- `Failed to convert WHERE clause to collection filter for collection '${this.collectionId}'. This indicates a bug in the query optimization logic.`
30
- );
31
23
  }
32
- } else {
33
- return this.subscribeToChanges();
24
+ throw new errors.WhereClauseConversionError(this.collectionId, this.alias);
34
25
  }
26
+ return this.subscribeToChanges();
35
27
  }
36
28
  subscribeToChanges(whereExpression) {
37
29
  let subscription;
38
- if (Object.hasOwn(
39
- this.collectionConfigBuilder.optimizableOrderByCollections,
40
- this.collectionId
41
- )) {
42
- subscription = this.subscribeToOrderedChanges(whereExpression);
30
+ const orderByInfo = this.getOrderByInfo();
31
+ if (orderByInfo) {
32
+ subscription = this.subscribeToOrderedChanges(
33
+ whereExpression,
34
+ orderByInfo
35
+ );
43
36
  } else {
44
- const includeInitialState = !this.collectionConfigBuilder.lazyCollections.has(this.collectionId);
37
+ const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(
38
+ this.alias
39
+ );
45
40
  subscription = this.subscribeToMatchingChanges(
46
41
  whereExpression,
47
42
  includeInitialState
@@ -50,22 +45,22 @@ class CollectionSubscriber {
50
45
  const unsubscribe = () => {
51
46
  subscription.unsubscribe();
52
47
  };
53
- this.syncState.unsubscribeCallbacks.add(unsubscribe);
48
+ this.collectionConfigBuilder.currentSyncState.unsubscribeCallbacks.add(
49
+ unsubscribe
50
+ );
54
51
  return subscription;
55
52
  }
56
53
  sendChangesToPipeline(changes, callback) {
57
- const input = this.syncState.inputs[this.collectionId];
54
+ const input = this.collectionConfigBuilder.currentSyncState.inputs[this.alias];
58
55
  const sentChanges = sendChangesToInput(
59
56
  input,
60
57
  changes,
61
58
  this.collection.config.getKey
62
59
  );
63
60
  const dataLoader = sentChanges > 0 ? callback : void 0;
64
- this.collectionConfigBuilder.maybeRunGraph(
65
- this.config,
66
- this.syncState,
67
- dataLoader
68
- );
61
+ this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {
62
+ alias: this.alias
63
+ });
69
64
  }
70
65
  subscribeToMatchingChanges(whereExpression, includeInitialState = false) {
71
66
  const sendChanges = (changes) => {
@@ -77,12 +72,12 @@ class CollectionSubscriber {
77
72
  });
78
73
  return subscription;
79
74
  }
80
- subscribeToOrderedChanges(whereExpression) {
81
- const { orderBy, offset, limit, comparator, dataNeeded, index } = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
75
+ subscribeToOrderedChanges(whereExpression, orderByInfo) {
76
+ const { orderBy, offset, limit, comparator, dataNeeded, index } = orderByInfo;
82
77
  const sendChangesInRange = (changes) => {
83
78
  const splittedChanges = splitUpdates(changes);
84
79
  let filteredChanges = splittedChanges;
85
- if (dataNeeded() === 0) {
80
+ if (dataNeeded && dataNeeded() === 0) {
86
81
  filteredChanges = filterChangesSmallerOrEqualToMax(
87
82
  splittedChanges,
88
83
  comparator,
@@ -97,7 +92,7 @@ class CollectionSubscriber {
97
92
  subscription.setOrderByIndex(index);
98
93
  const normalizedOrderBy = expressions.convertOrderByToBasicExpression(
99
94
  orderBy,
100
- this.collectionAlias
95
+ this.alias
101
96
  );
102
97
  subscription.requestLimitedSnapshot({
103
98
  limit: offset + limit,
@@ -109,7 +104,7 @@ class CollectionSubscriber {
109
104
  // after each iteration of the query pipeline
110
105
  // to ensure that the orderBy operator has enough data to work with
111
106
  loadMoreIfNeeded(subscription) {
112
- const orderByInfo = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
107
+ const orderByInfo = this.getOrderByInfo();
113
108
  if (!orderByInfo) {
114
109
  return true;
115
110
  }
@@ -126,22 +121,32 @@ class CollectionSubscriber {
126
121
  return true;
127
122
  }
128
123
  sendChangesToPipelineWithTracking(changes, subscription) {
129
- const { comparator } = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
130
- const trackedChanges = this.trackSentValues(changes, comparator);
124
+ const orderByInfo = this.getOrderByInfo();
125
+ if (!orderByInfo) {
126
+ this.sendChangesToPipeline(changes);
127
+ return;
128
+ }
129
+ const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator);
130
+ const subscriptionWithLoader = subscription;
131
+ subscriptionWithLoader[loadMoreCallbackSymbol] ??= this.loadMoreIfNeeded.bind(this, subscription);
131
132
  this.sendChangesToPipeline(
132
133
  trackedChanges,
133
- this.loadMoreIfNeeded.bind(this, subscription)
134
+ subscriptionWithLoader[loadMoreCallbackSymbol]
134
135
  );
135
136
  }
136
137
  // Loads the next `n` items from the collection
137
138
  // starting from the biggest item it has sent
138
139
  loadNextItems(n, subscription) {
139
- const { orderBy, valueExtractorForRawRow } = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
140
+ const orderByInfo = this.getOrderByInfo();
141
+ if (!orderByInfo) {
142
+ return;
143
+ }
144
+ const { orderBy, valueExtractorForRawRow } = orderByInfo;
140
145
  const biggestSentRow = this.biggest;
141
146
  const biggestSentValue = biggestSentRow ? valueExtractorForRawRow(biggestSentRow) : biggestSentRow;
142
147
  const normalizedOrderBy = expressions.convertOrderByToBasicExpression(
143
148
  orderBy,
144
- this.collectionAlias
149
+ this.alias
145
150
  );
146
151
  subscription.requestLimitedSnapshot({
147
152
  orderBy: normalizedOrderBy,
@@ -149,10 +154,17 @@ class CollectionSubscriber {
149
154
  minValue: biggestSentValue
150
155
  });
151
156
  }
152
- getWhereClauseFromAlias(collectionAlias) {
153
- const collectionWhereClausesCache = this.collectionConfigBuilder.collectionWhereClausesCache;
154
- if (collectionAlias && collectionWhereClausesCache) {
155
- return collectionWhereClausesCache.get(collectionAlias);
157
+ getWhereClauseForAlias() {
158
+ const sourceWhereClausesCache = this.collectionConfigBuilder.sourceWhereClausesCache;
159
+ if (!sourceWhereClausesCache) {
160
+ return void 0;
161
+ }
162
+ return sourceWhereClausesCache.get(this.alias);
163
+ }
164
+ getOrderByInfo() {
165
+ const info = this.collectionConfigBuilder.optimizableOrderByCollections[this.collectionId];
166
+ if (info && info.alias === this.alias) {
167
+ return info;
156
168
  }
157
169
  return void 0;
158
170
  }
@@ -167,19 +179,6 @@ class CollectionSubscriber {
167
179
  }
168
180
  }
169
181
  }
170
- function findCollectionAlias(collectionId, query) {
171
- if (query.from?.type === `collectionRef` && query.from.collection?.id === collectionId) {
172
- return query.from.alias;
173
- }
174
- if (query.join) {
175
- for (const joinClause of query.join) {
176
- if (joinClause.from?.type === `collectionRef` && joinClause.from.collection?.id === collectionId) {
177
- return joinClause.from.alias;
178
- }
179
- }
180
- }
181
- return void 0;
182
- }
183
182
  function sendChangesToInput(input, changes, getKey) {
184
183
  const multiSetArray = [];
185
184
  for (const change of changes) {
@@ -1 +1 @@
1
- {"version":3,"file":"collection-subscriber.cjs","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet } from \"@tanstack/db-ivm\"\nimport {\n convertOrderByToBasicExpression,\n convertToBasicExpression,\n} 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 private collectionAlias: string\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 this.collectionAlias = findCollectionAlias(\n this.collectionId,\n this.collectionConfigBuilder.query\n )!\n }\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseFromAlias(this.collectionAlias)\n\n if (whereClause) {\n // Convert WHERE clause to BasicExpression format for collection subscription\n const whereExpression = convertToBasicExpression(\n whereClause,\n this.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 { orderBy, 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 // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = convertOrderByToBasicExpression(\n orderBy,\n this.collectionAlias\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 subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\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 { orderBy, valueExtractorForRawRow } =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]!\n const biggestSentRow = this.biggest\n const biggestSentValue = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : biggestSentRow\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = convertOrderByToBasicExpression(\n orderBy,\n this.collectionAlias\n )\n\n // Take the `n` items after the biggest sent value\n subscription.requestLimitedSnapshot({\n orderBy: normalizedOrderBy,\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":["convertToBasicExpression","convertOrderByToBasicExpression","MultiSet"],"mappings":"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"}
1
+ {"version":3,"file":"collection-subscriber.cjs","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet } from \"@tanstack/db-ivm\"\nimport {\n convertOrderByToBasicExpression,\n convertToBasicExpression,\n} from \"../compiler/expressions.js\"\nimport { WhereClauseConversionError } from \"../../errors.js\"\nimport type { MultiSetArray, RootStreamBuilder } from \"@tanstack/db-ivm\"\nimport type { Collection } from \"../../collection/index.js\"\nimport type { ChangeMessage } from \"../../types.js\"\nimport type { Context, GetResult } from \"../builder/types.js\"\nimport type { BasicExpression } from \"../ir.js\"\nimport type { OrderByOptimizationInfo } from \"../compiler/order-by.js\"\nimport type { CollectionConfigBuilder } from \"./collection-config-builder.js\"\nimport type { CollectionSubscription } from \"../../collection/subscription.js\"\n\nconst loadMoreCallbackSymbol = Symbol.for(\n `@tanstack/db.collection-config-builder`\n)\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 alias: string,\n private collectionId: string,\n private collection: Collection,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>\n ) {}\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseForAlias()\n\n if (whereClause) {\n const whereExpression = convertToBasicExpression(whereClause, this.alias)\n\n if (whereExpression) {\n return this.subscribeToChanges(whereExpression)\n }\n\n throw new WhereClauseConversionError(this.collectionId, this.alias)\n }\n\n return this.subscribeToChanges()\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n let subscription: CollectionSubscription\n const orderByInfo = this.getOrderByInfo()\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo\n )\n } else {\n // If the source alias is lazy then we should not include the initial state\n const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(\n this.alias\n )\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState\n )\n }\n const unsubscribe = () => {\n subscription.unsubscribe()\n }\n // currentSyncState is always defined when subscribe() is called\n // (called during sync session setup)\n this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(\n unsubscribe\n )\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean\n ) {\n // currentSyncState and input are always defined when this method is called\n // (only called from active subscriptions during a sync session)\n const input =\n this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!\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 schedule a graph run 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 `scheduleGraphRun`\n this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {\n alias: this.alias,\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 orderByInfo: OrderByOptimizationInfo\n ) {\n const { orderBy, offset, limit, comparator, dataNeeded, index } =\n orderByInfo\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 && 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 // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = convertOrderByToBasicExpression(\n orderBy,\n this.alias\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 subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\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 = this.getOrderByInfo()\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 orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n this.sendChangesToPipeline(changes)\n return\n }\n\n const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)\n\n // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.\n // This ensures we pass the same function instance to the scheduler each time,\n // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.\n type SubscriptionWithLoader = CollectionSubscription & {\n [loadMoreCallbackSymbol]?: () => boolean\n }\n\n const subscriptionWithLoader = subscription as SubscriptionWithLoader\n\n subscriptionWithLoader[loadMoreCallbackSymbol] ??=\n this.loadMoreIfNeeded.bind(this, subscription)\n\n this.sendChangesToPipeline(\n trackedChanges,\n subscriptionWithLoader[loadMoreCallbackSymbol]\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 orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n return\n }\n const { orderBy, valueExtractorForRawRow } = orderByInfo\n const biggestSentRow = this.biggest\n const biggestSentValue = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : biggestSentRow\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = convertOrderByToBasicExpression(\n orderBy,\n this.alias\n )\n\n // Take the `n` items after the biggest sent value\n subscription.requestLimitedSnapshot({\n orderBy: normalizedOrderBy,\n limit: n,\n minValue: biggestSentValue,\n })\n }\n\n private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {\n const sourceWhereClausesCache =\n this.collectionConfigBuilder.sourceWhereClausesCache\n if (!sourceWhereClausesCache) {\n return undefined\n }\n return sourceWhereClausesCache.get(this.alias)\n }\n\n private getOrderByInfo(): OrderByOptimizationInfo | undefined {\n const info =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n if (info && info.alias === this.alias) {\n return info\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 * 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":["convertToBasicExpression","WhereClauseConversionError","convertOrderByToBasicExpression","MultiSet"],"mappings":"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"}
@@ -1,18 +1,14 @@
1
- import { FullSyncState } from './types.js';
2
1
  import { Collection } from '../../collection/index.js';
3
- import { SyncConfig } from '../../types.js';
4
2
  import { Context, GetResult } from '../builder/types.js';
5
3
  import { CollectionConfigBuilder } from './collection-config-builder.js';
6
4
  import { CollectionSubscription } from '../../collection/subscription.js';
7
5
  export declare class CollectionSubscriber<TContext extends Context, TResult extends object = GetResult<TContext>> {
6
+ private alias;
8
7
  private collectionId;
9
8
  private collection;
10
- private config;
11
- private syncState;
12
9
  private collectionConfigBuilder;
13
10
  private biggest;
14
- private collectionAlias;
15
- constructor(collectionId: string, collection: Collection, config: Parameters<SyncConfig<TResult>[`sync`]>[0], syncState: FullSyncState, collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>);
11
+ constructor(alias: string, collectionId: string, collection: Collection, collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>);
16
12
  subscribe(): CollectionSubscription;
17
13
  private subscribeToChanges;
18
14
  private sendChangesToPipeline;
@@ -21,6 +17,7 @@ export declare class CollectionSubscriber<TContext extends Context, TResult exte
21
17
  loadMoreIfNeeded(subscription: CollectionSubscription): boolean;
22
18
  private sendChangesToPipelineWithTracking;
23
19
  private loadNextItems;
24
- private getWhereClauseFromAlias;
20
+ private getWhereClauseForAlias;
21
+ private getOrderByInfo;
25
22
  private trackSentValues;
26
23
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const index = require("../collection/index.cjs");
4
4
  const collectionConfigBuilder = require("./live/collection-config-builder.cjs");
5
+ const collectionRegistry = require("./live/collection-registry.cjs");
5
6
  function liveQueryCollectionOptions(config) {
6
7
  const collectionConfigBuilder$1 = new collectionConfigBuilder.CollectionConfigBuilder(config);
7
8
  return collectionConfigBuilder$1.getConfig();
@@ -16,14 +17,19 @@ function createLiveQueryCollection(configOrQuery) {
16
17
  } else {
17
18
  const config = configOrQuery;
18
19
  const options = liveQueryCollectionOptions(config);
19
- return bridgeToCreateCollection({
20
- ...options,
21
- utils: config.utils
22
- });
20
+ if (config.utils) {
21
+ options.utils = { ...options.utils, ...config.utils };
22
+ }
23
+ return bridgeToCreateCollection(options);
23
24
  }
24
25
  }
25
26
  function bridgeToCreateCollection(options) {
26
- return index.createCollection(options);
27
+ const collection = index.createCollection(options);
28
+ const builder = collectionRegistry.getBuilderFromConfig(options);
29
+ if (builder) {
30
+ collectionRegistry.registerCollectionBuilder(collection, builder);
31
+ }
32
+ return collection;
27
33
  }
28
34
  exports.createLiveQueryCollection = createLiveQueryCollection;
29
35
  exports.liveQueryCollectionOptions = liveQueryCollectionOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"live-query-collection.cjs","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 {\n CollectionConfig,\n CollectionConfigSingleRowOption,\n NonSingleResult,\n SingleResult,\n UtilsRecord,\n} from \"../types.js\"\nimport type { Context, GetResult } from \"./builder/types.js\"\n\ntype CollectionConfigForContext<\n TContext extends Context,\n TResult extends object,\n> = TContext extends SingleResult\n ? CollectionConfigSingleRowOption<TResult> & SingleResult\n : CollectionConfigSingleRowOption<TResult> & NonSingleResult\n\ntype CollectionForContext<\n TContext extends Context,\n TResult extends object,\n> = TContext extends SingleResult\n ? Collection<TResult> & SingleResult\n : Collection<TResult> & NonSingleResult\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): CollectionConfigForContext<TContext, TResult> {\n const collectionConfigBuilder = new CollectionConfigBuilder<\n TContext,\n TResult\n >(config)\n return collectionConfigBuilder.getConfig() as CollectionConfigForContext<\n TContext,\n TResult\n >\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): CollectionForContext<TContext, TResult>\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): CollectionForContext<TContext, TResult>\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): CollectionForContext<TContext, TResult> {\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) as CollectionForContext<\n TContext,\n TResult\n >\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 }) as CollectionForContext<TContext, TResult>\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":["collectionConfigBuilder","CollectionConfigBuilder","createCollection"],"mappings":";;;;AAoDO,SAAS,2BAId,QAC+C;AAC/C,QAAMA,4BAA0B,IAAIC,wBAAAA,wBAGlC,MAAM;AACR,SAAOD,0BAAwB,UAAA;AAIjC;AAsDO,SAAS,0BAKd,eAGyC;AAEzC,MAAI,OAAO,kBAAkB,YAAY;AAEvC,UAAM,SAAuD;AAAA,MAC3D,OAAO;AAAA,IAAA;AAIT,UAAM,UAAU,2BAA8C,MAAM;AACpE,WAAO,yBAAyB,OAAO;AAAA,EAIzC,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,SAAOE,MAAAA,iBAAiB,OAAc;AAKxC;;;"}
1
+ {"version":3,"file":"live-query-collection.cjs","sources":["../../../src/query/live-query-collection.ts"],"sourcesContent":["import { createCollection } from \"../collection/index.js\"\nimport { CollectionConfigBuilder } from \"./live/collection-config-builder.js\"\nimport {\n getBuilderFromConfig,\n registerCollectionBuilder,\n} from \"./live/collection-registry.js\"\nimport type { LiveQueryCollectionUtils } 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 {\n CollectionConfig,\n CollectionConfigSingleRowOption,\n NonSingleResult,\n SingleResult,\n UtilsRecord,\n} from \"../types.js\"\nimport type { Context, GetResult } from \"./builder/types.js\"\n\ntype CollectionConfigForContext<\n TContext extends Context,\n TResult extends object,\n> = TContext extends SingleResult\n ? CollectionConfigSingleRowOption<TResult> & SingleResult\n : CollectionConfigSingleRowOption<TResult> & NonSingleResult\n\ntype CollectionForContext<\n TContext extends Context,\n TResult extends object,\n> = TContext extends SingleResult\n ? Collection<TResult> & SingleResult\n : Collection<TResult> & NonSingleResult\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): CollectionConfigForContext<TContext, TResult> & {\n utils: LiveQueryCollectionUtils\n} {\n const collectionConfigBuilder = new CollectionConfigBuilder<\n TContext,\n TResult\n >(config)\n return collectionConfigBuilder.getConfig() as CollectionConfigForContext<\n TContext,\n TResult\n > & { utils: LiveQueryCollectionUtils }\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): CollectionForContext<TContext, TResult> & {\n utils: LiveQueryCollectionUtils\n}\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): CollectionForContext<TContext, TResult> & {\n utils: LiveQueryCollectionUtils & TUtils\n}\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): CollectionForContext<TContext, TResult> & {\n utils: LiveQueryCollectionUtils & TUtils\n} {\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) as CollectionForContext<\n TContext,\n TResult\n > & { utils: LiveQueryCollectionUtils & TUtils }\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\n // Merge custom utils if provided, preserving the getBuilder() method for dependency tracking\n if (config.utils) {\n options.utils = { ...options.utils, ...config.utils }\n }\n\n return bridgeToCreateCollection(options) as CollectionForContext<\n TContext,\n TResult\n > & { utils: LiveQueryCollectionUtils & TUtils }\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 const collection = createCollection(options as any) as unknown as Collection<\n TResult,\n string | number,\n LiveQueryCollectionUtils\n >\n\n const builder = getBuilderFromConfig(options)\n if (builder) {\n registerCollectionBuilder(collection, builder)\n }\n\n return collection as unknown as Collection<TResult, string | number, TUtils>\n}\n"],"names":["collectionConfigBuilder","CollectionConfigBuilder","createCollection","getBuilderFromConfig","registerCollectionBuilder"],"mappings":";;;;;AAyDO,SAAS,2BAId,QAGA;AACA,QAAMA,4BAA0B,IAAIC,wBAAAA,wBAGlC,MAAM;AACR,SAAOD,0BAAwB,UAAA;AAIjC;AA0DO,SAAS,0BAKd,eAKA;AAEA,MAAI,OAAO,kBAAkB,YAAY;AAEvC,UAAM,SAAuD;AAAA,MAC3D,OAAO;AAAA,IAAA;AAIT,UAAM,UAAU,2BAA8C,MAAM;AACpE,WAAO,yBAAyB,OAAO;AAAA,EAIzC,OAAO;AAEL,UAAM,SAAS;AAIf,UAAM,UAAU,2BAA8C,MAAM;AAGpE,QAAI,OAAO,OAAO;AAChB,cAAQ,QAAQ,EAAE,GAAG,QAAQ,OAAO,GAAG,OAAO,MAAA;AAAA,IAChD;AAEA,WAAO,yBAAyB,OAAO;AAAA,EAIzC;AACF;AAMA,SAAS,yBAIP,SAC8C;AAC9C,QAAM,aAAaE,MAAAA,iBAAiB,OAAc;AAMlD,QAAM,UAAUC,mBAAAA,qBAAqB,OAAO;AAC5C,MAAI,SAAS;AACXC,uBAAAA,0BAA0B,YAAY,OAAO;AAAA,EAC/C;AAEA,SAAO;AACT;;;"}
@@ -1,3 +1,4 @@
1
+ import { LiveQueryCollectionUtils } from './live/collection-config-builder.js';
1
2
  import { LiveQueryCollectionConfig } from './live/types.js';
2
3
  import { InitialQueryBuilder, QueryBuilder } from './builder/index.js';
3
4
  import { Collection } from '../collection/index.js';
@@ -29,7 +30,9 @@ type CollectionForContext<TContext extends Context, TResult extends object> = TC
29
30
  * @param config - Configuration options for the live query collection
30
31
  * @returns Collection options that can be passed to createCollection
31
32
  */
32
- export declare function liveQueryCollectionOptions<TContext extends Context, TResult extends object = GetResult<TContext>>(config: LiveQueryCollectionConfig<TContext, TResult>): CollectionConfigForContext<TContext, TResult>;
33
+ export declare function liveQueryCollectionOptions<TContext extends Context, TResult extends object = GetResult<TContext>>(config: LiveQueryCollectionConfig<TContext, TResult>): CollectionConfigForContext<TContext, TResult> & {
34
+ utils: LiveQueryCollectionUtils;
35
+ };
33
36
  /**
34
37
  * Creates a live query collection directly
35
38
  *
@@ -63,8 +66,12 @@ export declare function liveQueryCollectionOptions<TContext extends Context, TRe
63
66
  * })
64
67
  * ```
65
68
  */
66
- export declare function createLiveQueryCollection<TContext extends Context, TResult extends object = GetResult<TContext>>(query: (q: InitialQueryBuilder) => QueryBuilder<TContext>): CollectionForContext<TContext, TResult>;
69
+ export declare function createLiveQueryCollection<TContext extends Context, TResult extends object = GetResult<TContext>>(query: (q: InitialQueryBuilder) => QueryBuilder<TContext>): CollectionForContext<TContext, TResult> & {
70
+ utils: LiveQueryCollectionUtils;
71
+ };
67
72
  export declare function createLiveQueryCollection<TContext extends Context, TResult extends object = GetResult<TContext>, TUtils extends UtilsRecord = {}>(config: LiveQueryCollectionConfig<TContext, TResult> & {
68
73
  utils?: TUtils;
69
- }): CollectionForContext<TContext, TResult>;
74
+ }): CollectionForContext<TContext, TResult> & {
75
+ utils: LiveQueryCollectionUtils & TUtils;
76
+ };
70
77
  export {};