@tanstack/db 0.4.5 → 0.4.6

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 (70) hide show
  1. package/dist/cjs/collection/change-events.cjs +1 -1
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/change-events.d.cts +1 -1
  4. package/dist/cjs/collection/index.cjs +11 -0
  5. package/dist/cjs/collection/index.cjs.map +1 -1
  6. package/dist/cjs/collection/index.d.cts +8 -1
  7. package/dist/cjs/collection/lifecycle.cjs +4 -1
  8. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  9. package/dist/cjs/collection/subscription.cjs +21 -1
  10. package/dist/cjs/collection/subscription.cjs.map +1 -1
  11. package/dist/cjs/collection/subscription.d.cts +4 -3
  12. package/dist/cjs/collection/sync.cjs +94 -71
  13. package/dist/cjs/collection/sync.cjs.map +1 -1
  14. package/dist/cjs/collection/sync.d.cts +9 -1
  15. package/dist/cjs/index.cjs +2 -0
  16. package/dist/cjs/index.cjs.map +1 -1
  17. package/dist/cjs/index.d.cts +2 -0
  18. package/dist/cjs/indexes/auto-index.cjs +4 -1
  19. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  20. package/dist/cjs/query/compiler/expressions.cjs +19 -0
  21. package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
  22. package/dist/cjs/query/compiler/expressions.d.cts +2 -1
  23. package/dist/cjs/query/compiler/order-by.cjs +2 -1
  24. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  25. package/dist/cjs/query/compiler/order-by.d.cts +2 -1
  26. package/dist/cjs/query/live/collection-subscriber.cjs +18 -8
  27. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  28. package/dist/cjs/query/live/collection-subscriber.d.cts +1 -0
  29. package/dist/cjs/types.d.cts +11 -1
  30. package/dist/esm/collection/change-events.d.ts +1 -1
  31. package/dist/esm/collection/change-events.js +1 -1
  32. package/dist/esm/collection/change-events.js.map +1 -1
  33. package/dist/esm/collection/index.d.ts +8 -1
  34. package/dist/esm/collection/index.js +11 -0
  35. package/dist/esm/collection/index.js.map +1 -1
  36. package/dist/esm/collection/lifecycle.js +4 -1
  37. package/dist/esm/collection/lifecycle.js.map +1 -1
  38. package/dist/esm/collection/subscription.d.ts +4 -3
  39. package/dist/esm/collection/subscription.js +22 -2
  40. package/dist/esm/collection/subscription.js.map +1 -1
  41. package/dist/esm/collection/sync.d.ts +9 -1
  42. package/dist/esm/collection/sync.js +94 -71
  43. package/dist/esm/collection/sync.js.map +1 -1
  44. package/dist/esm/index.d.ts +2 -0
  45. package/dist/esm/index.js +2 -0
  46. package/dist/esm/index.js.map +1 -1
  47. package/dist/esm/indexes/auto-index.js +4 -1
  48. package/dist/esm/indexes/auto-index.js.map +1 -1
  49. package/dist/esm/query/compiler/expressions.d.ts +2 -1
  50. package/dist/esm/query/compiler/expressions.js +19 -0
  51. package/dist/esm/query/compiler/expressions.js.map +1 -1
  52. package/dist/esm/query/compiler/order-by.d.ts +2 -1
  53. package/dist/esm/query/compiler/order-by.js +2 -1
  54. package/dist/esm/query/compiler/order-by.js.map +1 -1
  55. package/dist/esm/query/live/collection-subscriber.d.ts +1 -0
  56. package/dist/esm/query/live/collection-subscriber.js +19 -9
  57. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  58. package/dist/esm/types.d.ts +11 -1
  59. package/package.json +1 -1
  60. package/src/collection/change-events.ts +5 -2
  61. package/src/collection/index.ts +13 -0
  62. package/src/collection/lifecycle.ts +4 -1
  63. package/src/collection/subscription.ts +34 -4
  64. package/src/collection/sync.ts +147 -110
  65. package/src/index.ts +5 -0
  66. package/src/indexes/auto-index.ts +4 -1
  67. package/src/query/compiler/expressions.ts +26 -1
  68. package/src/query/compiler/order-by.ts +3 -1
  69. package/src/query/live/collection-subscriber.ts +31 -10
  70. package/src/types.ts +13 -1
@@ -1 +1 @@
1
- {"version":3,"file":"subscription.js","sources":["../../../src/collection/subscription.ts"],"sourcesContent":["import { ensureIndexForExpression } from \"../indexes/auto-index.js\"\nimport { and } from \"../query/builder/functions.js\"\nimport {\n createFilterFunctionFromExpression,\n createFilteredCallback,\n} from \"./change-events.js\"\nimport type { BasicExpression } from \"../query/ir.js\"\nimport type { IndexInterface } from \"../indexes/base-index.js\"\nimport type { ChangeMessage } from \"../types.js\"\nimport type { CollectionImpl } from \"./index.js\"\n\ntype RequestSnapshotOptions = {\n where?: BasicExpression<boolean>\n optimizedOnly?: boolean\n}\n\ntype RequestLimitedSnapshotOptions = {\n minValue?: any\n limit: number\n}\n\ntype CollectionSubscriptionOptions = {\n /** Pre-compiled expression for filtering changes */\n whereExpression?: BasicExpression<boolean>\n /** Callback to call when the subscription is unsubscribed */\n onUnsubscribe?: () => void\n}\n\nexport class CollectionSubscription {\n private loadedInitialState = false\n\n // Flag to indicate that we have sent at least 1 snapshot.\n // While `snapshotSent` is false we filter out all changes from subscription to the collection.\n private snapshotSent = false\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 private filteredCallback: (changes: Array<ChangeMessage<any, any>>) => void\n\n private orderByIndex: IndexInterface<string | number> | undefined\n\n constructor(\n private collection: CollectionImpl<any, any, any, any, any>,\n private callback: (changes: Array<ChangeMessage<any, any>>) => void,\n private options: CollectionSubscriptionOptions\n ) {\n // Auto-index for where expressions if enabled\n if (options.whereExpression) {\n ensureIndexForExpression(options.whereExpression, this.collection)\n }\n\n const callbackWithSentKeysTracking = (\n changes: Array<ChangeMessage<any, any>>\n ) => {\n callback(changes)\n this.trackSentKeys(changes)\n }\n\n this.callback = callbackWithSentKeysTracking\n\n // Create a filtered callback if where clause is provided\n this.filteredCallback = options.whereExpression\n ? createFilteredCallback(this.callback, options)\n : this.callback\n }\n\n setOrderByIndex(index: IndexInterface<any>) {\n this.orderByIndex = index\n }\n\n hasLoadedInitialState() {\n return this.loadedInitialState\n }\n\n hasSentAtLeastOneSnapshot() {\n return this.snapshotSent\n }\n\n emitEvents(changes: Array<ChangeMessage<any, any>>) {\n const newChanges = this.filterAndFlipChanges(changes)\n this.filteredCallback(newChanges)\n }\n\n /**\n * Sends the snapshot to the callback.\n * Returns a boolean indicating if it succeeded.\n * It can only fail if there is no index to fulfill the request\n * and the optimizedOnly option is set to true,\n * or, the entire state was already loaded.\n */\n requestSnapshot(opts?: RequestSnapshotOptions): boolean {\n if (this.loadedInitialState) {\n // Subscription was deoptimized so we already sent the entire initial state\n return false\n }\n\n const stateOpts: RequestSnapshotOptions = {\n where: this.options.whereExpression,\n optimizedOnly: opts?.optimizedOnly ?? false,\n }\n\n if (opts) {\n if (`where` in opts) {\n const snapshotWhereExp = opts.where\n if (stateOpts.where) {\n // Combine the two where expressions\n const subWhereExp = stateOpts.where\n const combinedWhereExp = and(subWhereExp, snapshotWhereExp)\n stateOpts.where = combinedWhereExp\n } else {\n stateOpts.where = snapshotWhereExp\n }\n }\n } else {\n // No options provided so it's loading the entire initial state\n this.loadedInitialState = true\n }\n\n const snapshot = this.collection.currentStateAsChanges(stateOpts)\n\n if (snapshot === undefined) {\n // Couldn't load from indexes\n return false\n }\n\n // Only send changes that have not been sent yet\n const filteredSnapshot = snapshot.filter(\n (change) => !this.sentKeys.has(change.key)\n )\n\n this.snapshotSent = true\n this.callback(filteredSnapshot)\n return true\n }\n\n /**\n * Sends a snapshot that is limited to the first `limit` rows that fulfill the `where` clause and are bigger than `minValue`.\n * Requires a range index to be set with `setOrderByIndex` prior to calling this method.\n * It uses that range index to load the items in the order of the index.\n * Note: it does not send keys that have already been sent before.\n */\n requestLimitedSnapshot({ limit, minValue }: RequestLimitedSnapshotOptions) {\n if (!limit) throw new Error(`limit is required`)\n\n if (!this.orderByIndex) {\n throw new Error(\n `Ordered snapshot was requested but no index was found. You have to call setOrderByIndex before requesting an ordered snapshot.`\n )\n }\n\n const index = this.orderByIndex\n const where = this.options.whereExpression\n const whereFilterFn = where\n ? createFilterFunctionFromExpression(where)\n : undefined\n\n const filterFn = (key: string | number): boolean => {\n if (this.sentKeys.has(key)) {\n return false\n }\n\n const value = this.collection.get(key)\n if (value === undefined) {\n return false\n }\n\n return whereFilterFn?.(value) ?? true\n }\n\n let biggestObservedValue = minValue\n const changes: Array<ChangeMessage<any, string | number>> = []\n let keys: Array<string | number> = index.take(limit, minValue, filterFn)\n\n const valuesNeeded = () => Math.max(limit - changes.length, 0)\n const collectionExhausted = () => keys.length === 0\n\n while (valuesNeeded() > 0 && !collectionExhausted()) {\n for (const key of keys) {\n const value = this.collection.get(key)!\n changes.push({\n type: `insert`,\n key,\n value,\n })\n biggestObservedValue = value\n }\n\n keys = index.take(valuesNeeded(), biggestObservedValue, filterFn)\n }\n\n this.callback(changes)\n }\n\n /**\n * Filters and flips changes for keys that have not been sent yet.\n * Deletes are filtered out for keys that have not been sent yet.\n * Updates are flipped into inserts for keys that have not been sent yet.\n */\n private filterAndFlipChanges(changes: Array<ChangeMessage<any, any>>) {\n if (this.loadedInitialState) {\n // We loaded the entire initial state\n // so no need to filter or flip changes\n return 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`, previousValue: undefined }\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 return newChanges\n }\n\n private trackSentKeys(changes: Array<ChangeMessage<any, string | number>>) {\n if (this.loadedInitialState) {\n // No need to track sent keys if we loaded the entire state.\n // Since we sent everything, all keys must have been observed.\n return\n }\n\n for (const change of changes) {\n this.sentKeys.add(change.key)\n }\n }\n\n unsubscribe() {\n this.options.onUnsubscribe?.()\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subscription.js","sources":["../../../src/collection/subscription.ts"],"sourcesContent":["import { ensureIndexForExpression } from \"../indexes/auto-index.js\"\nimport { and, gt, lt } from \"../query/builder/functions.js\"\nimport { Value } from \"../query/ir.js\"\nimport {\n createFilterFunctionFromExpression,\n createFilteredCallback,\n} from \"./change-events.js\"\nimport type { BasicExpression, OrderBy } from \"../query/ir.js\"\nimport type { IndexInterface } from \"../indexes/base-index.js\"\nimport type { ChangeMessage } from \"../types.js\"\nimport type { CollectionImpl } from \"./index.js\"\n\ntype RequestSnapshotOptions = {\n where?: BasicExpression<boolean>\n optimizedOnly?: boolean\n}\n\ntype RequestLimitedSnapshotOptions = {\n orderBy: OrderBy\n limit: number\n minValue?: any\n}\n\ntype CollectionSubscriptionOptions = {\n /** Pre-compiled expression for filtering changes */\n whereExpression?: BasicExpression<boolean>\n /** Callback to call when the subscription is unsubscribed */\n onUnsubscribe?: () => void\n}\n\nexport class CollectionSubscription {\n private loadedInitialState = false\n\n // Flag to indicate that we have sent at least 1 snapshot.\n // While `snapshotSent` is false we filter out all changes from subscription to the collection.\n private snapshotSent = false\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 private filteredCallback: (changes: Array<ChangeMessage<any, any>>) => void\n\n private orderByIndex: IndexInterface<string | number> | undefined\n\n constructor(\n private collection: CollectionImpl<any, any, any, any, any>,\n private callback: (changes: Array<ChangeMessage<any, any>>) => void,\n private options: CollectionSubscriptionOptions\n ) {\n // Auto-index for where expressions if enabled\n if (options.whereExpression) {\n ensureIndexForExpression(options.whereExpression, this.collection)\n }\n\n const callbackWithSentKeysTracking = (\n changes: Array<ChangeMessage<any, any>>\n ) => {\n callback(changes)\n this.trackSentKeys(changes)\n }\n\n this.callback = callbackWithSentKeysTracking\n\n // Create a filtered callback if where clause is provided\n this.filteredCallback = options.whereExpression\n ? createFilteredCallback(this.callback, options)\n : this.callback\n }\n\n setOrderByIndex(index: IndexInterface<any>) {\n this.orderByIndex = index\n }\n\n hasLoadedInitialState() {\n return this.loadedInitialState\n }\n\n hasSentAtLeastOneSnapshot() {\n return this.snapshotSent\n }\n\n emitEvents(changes: Array<ChangeMessage<any, any>>) {\n const newChanges = this.filterAndFlipChanges(changes)\n this.filteredCallback(newChanges)\n }\n\n /**\n * Sends the snapshot to the callback.\n * Returns a boolean indicating if it succeeded.\n * It can only fail if there is no index to fulfill the request\n * and the optimizedOnly option is set to true,\n * or, the entire state was already loaded.\n */\n requestSnapshot(opts?: RequestSnapshotOptions): boolean {\n if (this.loadedInitialState) {\n // Subscription was deoptimized so we already sent the entire initial state\n return false\n }\n\n const stateOpts: RequestSnapshotOptions = {\n where: this.options.whereExpression,\n optimizedOnly: opts?.optimizedOnly ?? false,\n }\n\n if (opts) {\n if (`where` in opts) {\n const snapshotWhereExp = opts.where\n if (stateOpts.where) {\n // Combine the two where expressions\n const subWhereExp = stateOpts.where\n const combinedWhereExp = and(subWhereExp, snapshotWhereExp)\n stateOpts.where = combinedWhereExp\n } else {\n stateOpts.where = snapshotWhereExp\n }\n }\n } else {\n // No options provided so it's loading the entire initial state\n this.loadedInitialState = true\n }\n\n // Request the sync layer to load more data\n // don't await it, we will load the data into the collection when it comes in\n this.collection.syncMore({\n where: stateOpts.where,\n })\n\n // Also load data immediately from the collection\n const snapshot = this.collection.currentStateAsChanges(stateOpts)\n\n if (snapshot === undefined) {\n // Couldn't load from indexes\n return false\n }\n\n // Only send changes that have not been sent yet\n const filteredSnapshot = snapshot.filter(\n (change) => !this.sentKeys.has(change.key)\n )\n\n this.snapshotSent = true\n this.callback(filteredSnapshot)\n return true\n }\n\n /**\n * Sends a snapshot that is limited to the first `limit` rows that fulfill the `where` clause and are bigger than `minValue`.\n * Requires a range index to be set with `setOrderByIndex` prior to calling this method.\n * It uses that range index to load the items in the order of the index.\n * Note: it does not send keys that have already been sent before.\n */\n requestLimitedSnapshot({\n orderBy,\n limit,\n minValue,\n }: RequestLimitedSnapshotOptions) {\n if (!limit) throw new Error(`limit is required`)\n\n if (!this.orderByIndex) {\n throw new Error(\n `Ordered snapshot was requested but no index was found. You have to call setOrderByIndex before requesting an ordered snapshot.`\n )\n }\n\n const index = this.orderByIndex\n const where = this.options.whereExpression\n const whereFilterFn = where\n ? createFilterFunctionFromExpression(where)\n : undefined\n\n const filterFn = (key: string | number): boolean => {\n if (this.sentKeys.has(key)) {\n return false\n }\n\n const value = this.collection.get(key)\n if (value === undefined) {\n return false\n }\n\n return whereFilterFn?.(value) ?? true\n }\n\n let biggestObservedValue = minValue\n const changes: Array<ChangeMessage<any, string | number>> = []\n let keys: Array<string | number> = index.take(limit, minValue, filterFn)\n\n const valuesNeeded = () => Math.max(limit - changes.length, 0)\n const collectionExhausted = () => keys.length === 0\n\n while (valuesNeeded() > 0 && !collectionExhausted()) {\n for (const key of keys) {\n const value = this.collection.get(key)!\n changes.push({\n type: `insert`,\n key,\n value,\n })\n biggestObservedValue = value\n }\n\n keys = index.take(valuesNeeded(), biggestObservedValue, filterFn)\n }\n\n this.callback(changes)\n\n let whereWithValueFilter = where\n if (typeof minValue !== `undefined`) {\n // Only request data that we haven't seen yet (i.e. is bigger than the minValue)\n const { expression, compareOptions } = orderBy[0]!\n const operator = compareOptions.direction === `asc` ? gt : lt\n const valueFilter = operator(expression, new Value(minValue))\n whereWithValueFilter = where ? and(where, valueFilter) : valueFilter\n }\n\n // Request the sync layer to load more data\n // don't await it, we will load the data into the collection when it comes in\n this.collection.syncMore({\n where: whereWithValueFilter,\n limit,\n orderBy,\n })\n }\n\n /**\n * Filters and flips changes for keys that have not been sent yet.\n * Deletes are filtered out for keys that have not been sent yet.\n * Updates are flipped into inserts for keys that have not been sent yet.\n */\n private filterAndFlipChanges(changes: Array<ChangeMessage<any, any>>) {\n if (this.loadedInitialState) {\n // We loaded the entire initial state\n // so no need to filter or flip changes\n return 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`, previousValue: undefined }\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 return newChanges\n }\n\n private trackSentKeys(changes: Array<ChangeMessage<any, string | number>>) {\n if (this.loadedInitialState) {\n // No need to track sent keys if we loaded the entire state.\n // Since we sent everything, all keys must have been observed.\n return\n }\n\n for (const change of changes) {\n this.sentKeys.add(change.key)\n }\n }\n\n unsubscribe() {\n this.options.onUnsubscribe?.()\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
- import { CollectionConfig } from '../types.js';
2
+ import { CollectionConfig, OnLoadMoreOptions } from '../types.js';
3
3
  import { CollectionImpl } from './index.js';
4
4
  import { CollectionStateManager } from './state.js';
5
5
  import { CollectionLifecycleManager } from './lifecycle.js';
@@ -11,6 +11,7 @@ export declare class CollectionSyncManager<TOutput extends object = Record<strin
11
11
  private id;
12
12
  preloadPromise: Promise<void> | null;
13
13
  syncCleanupFn: (() => void) | null;
14
+ syncOnLoadMoreFn: ((options: OnLoadMoreOptions) => void | Promise<void>) | null;
14
15
  /**
15
16
  * Creates a new CollectionSyncManager instance
16
17
  */
@@ -30,5 +31,12 @@ export declare class CollectionSyncManager<TOutput extends object = Record<strin
30
31
  * Multiple concurrent calls will share the same promise
31
32
  */
32
33
  preload(): Promise<void>;
34
+ /**
35
+ * Requests the sync layer to load more data.
36
+ * @param options Options to control what data is being loaded
37
+ * @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.
38
+ * If data loading is synchronous, the data is loaded when the method returns.
39
+ */
40
+ syncMore(options: OnLoadMoreOptions): void | Promise<void>;
33
41
  cleanup(): void;
34
42
  }
@@ -7,6 +7,7 @@ class CollectionSyncManager {
7
7
  constructor(config, id) {
8
8
  this.preloadPromise = null;
9
9
  this.syncCleanupFn = null;
10
+ this.syncOnLoadMoreFn = null;
10
11
  this.config = config;
11
12
  this.id = id;
12
13
  }
@@ -20,85 +21,87 @@ class CollectionSyncManager {
20
21
  * This is called when the collection is first accessed or preloaded
21
22
  */
22
23
  startSync() {
23
- const state = this.state;
24
24
  if (this.lifecycle.status !== `idle` && this.lifecycle.status !== `cleaned-up`) {
25
25
  return;
26
26
  }
27
27
  this.lifecycle.setStatus(`loading`);
28
28
  try {
29
- const cleanupFn = this.config.sync.sync({
30
- collection: this.collection,
31
- begin: () => {
32
- state.pendingSyncedTransactions.push({
33
- committed: false,
34
- operations: [],
35
- deletedKeys: /* @__PURE__ */ new Set()
36
- });
37
- },
38
- write: (messageWithoutKey) => {
39
- const pendingTransaction = state.pendingSyncedTransactions[state.pendingSyncedTransactions.length - 1];
40
- if (!pendingTransaction) {
41
- throw new NoPendingSyncTransactionWriteError();
42
- }
43
- if (pendingTransaction.committed) {
44
- throw new SyncTransactionAlreadyCommittedWriteError();
45
- }
46
- const key = this.config.getKey(messageWithoutKey.value);
47
- let messageType = messageWithoutKey.type;
48
- if (messageWithoutKey.type === `insert`) {
49
- const insertingIntoExistingSynced = state.syncedData.has(key);
50
- const hasPendingDeleteForKey = pendingTransaction.deletedKeys.has(key);
51
- const isTruncateTransaction = pendingTransaction.truncate === true;
52
- if (insertingIntoExistingSynced && !hasPendingDeleteForKey && !isTruncateTransaction) {
53
- const existingValue = state.syncedData.get(key);
54
- if (existingValue !== void 0 && deepEquals(existingValue, messageWithoutKey.value)) {
55
- messageType = `update`;
56
- } else {
57
- throw new DuplicateKeySyncError(key, this.id);
29
+ const syncRes = normalizeSyncFnResult(
30
+ this.config.sync.sync({
31
+ collection: this.collection,
32
+ begin: () => {
33
+ this.state.pendingSyncedTransactions.push({
34
+ committed: false,
35
+ operations: [],
36
+ deletedKeys: /* @__PURE__ */ new Set()
37
+ });
38
+ },
39
+ write: (messageWithoutKey) => {
40
+ const pendingTransaction = this.state.pendingSyncedTransactions[this.state.pendingSyncedTransactions.length - 1];
41
+ if (!pendingTransaction) {
42
+ throw new NoPendingSyncTransactionWriteError();
43
+ }
44
+ if (pendingTransaction.committed) {
45
+ throw new SyncTransactionAlreadyCommittedWriteError();
46
+ }
47
+ const key = this.config.getKey(messageWithoutKey.value);
48
+ let messageType = messageWithoutKey.type;
49
+ if (messageWithoutKey.type === `insert`) {
50
+ const insertingIntoExistingSynced = this.state.syncedData.has(key);
51
+ const hasPendingDeleteForKey = pendingTransaction.deletedKeys.has(key);
52
+ const isTruncateTransaction = pendingTransaction.truncate === true;
53
+ if (insertingIntoExistingSynced && !hasPendingDeleteForKey && !isTruncateTransaction) {
54
+ const existingValue = this.state.syncedData.get(key);
55
+ if (existingValue !== void 0 && deepEquals(existingValue, messageWithoutKey.value)) {
56
+ messageType = `update`;
57
+ } else {
58
+ throw new DuplicateKeySyncError(key, this.id);
59
+ }
58
60
  }
59
61
  }
62
+ const message = {
63
+ ...messageWithoutKey,
64
+ type: messageType,
65
+ key
66
+ };
67
+ pendingTransaction.operations.push(message);
68
+ if (messageType === `delete`) {
69
+ pendingTransaction.deletedKeys.add(key);
70
+ }
71
+ },
72
+ commit: () => {
73
+ const pendingTransaction = this.state.pendingSyncedTransactions[this.state.pendingSyncedTransactions.length - 1];
74
+ if (!pendingTransaction) {
75
+ throw new NoPendingSyncTransactionCommitError();
76
+ }
77
+ if (pendingTransaction.committed) {
78
+ throw new SyncTransactionAlreadyCommittedError();
79
+ }
80
+ pendingTransaction.committed = true;
81
+ if (this.lifecycle.status === `loading`) {
82
+ this.lifecycle.setStatus(`initialCommit`);
83
+ }
84
+ this.state.commitPendingTransactions();
85
+ },
86
+ markReady: () => {
87
+ this.lifecycle.markReady();
88
+ },
89
+ truncate: () => {
90
+ const pendingTransaction = this.state.pendingSyncedTransactions[this.state.pendingSyncedTransactions.length - 1];
91
+ if (!pendingTransaction) {
92
+ throw new NoPendingSyncTransactionWriteError();
93
+ }
94
+ if (pendingTransaction.committed) {
95
+ throw new SyncTransactionAlreadyCommittedWriteError();
96
+ }
97
+ pendingTransaction.operations = [];
98
+ pendingTransaction.deletedKeys.clear();
99
+ pendingTransaction.truncate = true;
60
100
  }
61
- const message = {
62
- ...messageWithoutKey,
63
- type: messageType,
64
- key
65
- };
66
- pendingTransaction.operations.push(message);
67
- if (messageType === `delete`) {
68
- pendingTransaction.deletedKeys.add(key);
69
- }
70
- },
71
- commit: () => {
72
- const pendingTransaction = state.pendingSyncedTransactions[state.pendingSyncedTransactions.length - 1];
73
- if (!pendingTransaction) {
74
- throw new NoPendingSyncTransactionCommitError();
75
- }
76
- if (pendingTransaction.committed) {
77
- throw new SyncTransactionAlreadyCommittedError();
78
- }
79
- pendingTransaction.committed = true;
80
- if (this.lifecycle.status === `loading`) {
81
- this.lifecycle.setStatus(`initialCommit`);
82
- }
83
- state.commitPendingTransactions();
84
- },
85
- markReady: () => {
86
- this.lifecycle.markReady();
87
- },
88
- truncate: () => {
89
- const pendingTransaction = state.pendingSyncedTransactions[state.pendingSyncedTransactions.length - 1];
90
- if (!pendingTransaction) {
91
- throw new NoPendingSyncTransactionWriteError();
92
- }
93
- if (pendingTransaction.committed) {
94
- throw new SyncTransactionAlreadyCommittedWriteError();
95
- }
96
- pendingTransaction.operations = [];
97
- pendingTransaction.deletedKeys.clear();
98
- pendingTransaction.truncate = true;
99
- }
100
- });
101
- this.syncCleanupFn = typeof cleanupFn === `function` ? cleanupFn : null;
101
+ })
102
+ );
103
+ this.syncCleanupFn = syncRes?.cleanup ?? null;
104
+ this.syncOnLoadMoreFn = syncRes?.onLoadMore ?? null;
102
105
  } catch (error) {
103
106
  this.lifecycle.setStatus(`error`);
104
107
  throw error;
@@ -135,6 +138,17 @@ class CollectionSyncManager {
135
138
  });
136
139
  return this.preloadPromise;
137
140
  }
141
+ /**
142
+ * Requests the sync layer to load more data.
143
+ * @param options Options to control what data is being loaded
144
+ * @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.
145
+ * If data loading is synchronous, the data is loaded when the method returns.
146
+ */
147
+ syncMore(options) {
148
+ if (this.syncOnLoadMoreFn) {
149
+ return this.syncOnLoadMoreFn(options);
150
+ }
151
+ }
138
152
  cleanup() {
139
153
  try {
140
154
  if (this.syncCleanupFn) {
@@ -156,6 +170,15 @@ class CollectionSyncManager {
156
170
  this.preloadPromise = null;
157
171
  }
158
172
  }
173
+ function normalizeSyncFnResult(result) {
174
+ if (typeof result === `function`) {
175
+ return { cleanup: result };
176
+ }
177
+ if (typeof result === `object`) {
178
+ return result;
179
+ }
180
+ return void 0;
181
+ }
159
182
  export {
160
183
  CollectionSyncManager
161
184
  };
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","sources":["../../../src/collection/sync.ts"],"sourcesContent":["import {\n CollectionIsInErrorStateError,\n DuplicateKeySyncError,\n NoPendingSyncTransactionCommitError,\n NoPendingSyncTransactionWriteError,\n SyncCleanupError,\n SyncTransactionAlreadyCommittedError,\n SyncTransactionAlreadyCommittedWriteError,\n} from \"../errors\"\nimport { deepEquals } from \"../utils\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type { ChangeMessage, CollectionConfig } from \"../types\"\nimport type { CollectionImpl } from \"./index.js\"\nimport type { CollectionStateManager } from \"./state\"\nimport type { CollectionLifecycleManager } from \"./lifecycle\"\n\nexport class CollectionSyncManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n private config!: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n\n public preloadPromise: Promise<void> | null = null\n public syncCleanupFn: (() => void) | null = null\n\n /**\n * Creates a new CollectionSyncManager instance\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.config = config\n this.id = id\n }\n\n setDeps(deps: {\n collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.collection = deps.collection\n this.state = deps.state\n this.lifecycle = deps.lifecycle\n }\n\n /**\n * Start the sync process for this collection\n * This is called when the collection is first accessed or preloaded\n */\n public startSync(): void {\n const state = this.state\n if (\n this.lifecycle.status !== `idle` &&\n this.lifecycle.status !== `cleaned-up`\n ) {\n return // Already started or in progress\n }\n\n this.lifecycle.setStatus(`loading`)\n\n try {\n const cleanupFn = this.config.sync.sync({\n collection: this.collection,\n begin: () => {\n state.pendingSyncedTransactions.push({\n committed: false,\n operations: [],\n deletedKeys: new Set(),\n })\n },\n write: (messageWithoutKey: Omit<ChangeMessage<TOutput>, `key`>) => {\n const pendingTransaction =\n state.pendingSyncedTransactions[\n state.pendingSyncedTransactions.length - 1\n ]\n if (!pendingTransaction) {\n throw new NoPendingSyncTransactionWriteError()\n }\n if (pendingTransaction.committed) {\n throw new SyncTransactionAlreadyCommittedWriteError()\n }\n const key = this.config.getKey(messageWithoutKey.value)\n\n let messageType = messageWithoutKey.type\n\n // Check if an item with this key already exists when inserting\n if (messageWithoutKey.type === `insert`) {\n const insertingIntoExistingSynced = state.syncedData.has(key)\n const hasPendingDeleteForKey =\n pendingTransaction.deletedKeys.has(key)\n const isTruncateTransaction = pendingTransaction.truncate === true\n // Allow insert after truncate in the same transaction even if it existed in syncedData\n if (\n insertingIntoExistingSynced &&\n !hasPendingDeleteForKey &&\n !isTruncateTransaction\n ) {\n const existingValue = state.syncedData.get(key)\n if (\n existingValue !== undefined &&\n deepEquals(existingValue, messageWithoutKey.value)\n ) {\n // The \"insert\" is an echo of a value we already have locally.\n // Treat it as an update so we preserve optimistic intent without\n // throwing a duplicate-key error during reconciliation.\n messageType = `update`\n } else {\n throw new DuplicateKeySyncError(key, this.id)\n }\n }\n }\n\n const message: ChangeMessage<TOutput> = {\n ...messageWithoutKey,\n type: messageType,\n key,\n }\n pendingTransaction.operations.push(message)\n\n if (messageType === `delete`) {\n pendingTransaction.deletedKeys.add(key)\n }\n },\n commit: () => {\n const pendingTransaction =\n state.pendingSyncedTransactions[\n state.pendingSyncedTransactions.length - 1\n ]\n if (!pendingTransaction) {\n throw new NoPendingSyncTransactionCommitError()\n }\n if (pendingTransaction.committed) {\n throw new SyncTransactionAlreadyCommittedError()\n }\n\n pendingTransaction.committed = true\n\n // Update status to initialCommit when transitioning from loading\n // This indicates we're in the process of committing the first transaction\n if (this.lifecycle.status === `loading`) {\n this.lifecycle.setStatus(`initialCommit`)\n }\n\n state.commitPendingTransactions()\n },\n markReady: () => {\n this.lifecycle.markReady()\n },\n truncate: () => {\n const pendingTransaction =\n state.pendingSyncedTransactions[\n state.pendingSyncedTransactions.length - 1\n ]\n if (!pendingTransaction) {\n throw new NoPendingSyncTransactionWriteError()\n }\n if (pendingTransaction.committed) {\n throw new SyncTransactionAlreadyCommittedWriteError()\n }\n\n // Clear all operations from the current transaction\n pendingTransaction.operations = []\n pendingTransaction.deletedKeys.clear()\n\n // Mark the transaction as a truncate operation. During commit, this triggers:\n // - Delete events for all previously synced keys (excluding optimistic-deleted keys)\n // - Clearing of syncedData/syncedMetadata\n // - Subsequent synced ops applied on the fresh base\n // - Finally, optimistic mutations re-applied on top (single batch)\n pendingTransaction.truncate = true\n },\n })\n\n // Store cleanup function if provided\n this.syncCleanupFn = typeof cleanupFn === `function` ? cleanupFn : null\n } catch (error) {\n this.lifecycle.setStatus(`error`)\n throw error\n }\n }\n\n /**\n * Preload the collection data by starting sync if not already started\n * Multiple concurrent calls will share the same promise\n */\n public preload(): Promise<void> {\n if (this.preloadPromise) {\n return this.preloadPromise\n }\n\n this.preloadPromise = new Promise<void>((resolve, reject) => {\n if (this.lifecycle.status === `ready`) {\n resolve()\n return\n }\n\n if (this.lifecycle.status === `error`) {\n reject(new CollectionIsInErrorStateError())\n return\n }\n\n // Register callback BEFORE starting sync to avoid race condition\n this.lifecycle.onFirstReady(() => {\n resolve()\n })\n\n // Start sync if collection hasn't started yet or was cleaned up\n if (\n this.lifecycle.status === `idle` ||\n this.lifecycle.status === `cleaned-up`\n ) {\n try {\n this.startSync()\n } catch (error) {\n reject(error)\n return\n }\n }\n })\n\n return this.preloadPromise\n }\n\n public cleanup(): void {\n try {\n if (this.syncCleanupFn) {\n this.syncCleanupFn()\n this.syncCleanupFn = null\n }\n } catch (error) {\n // Re-throw in a microtask to surface the error after cleanup completes\n queueMicrotask(() => {\n if (error instanceof Error) {\n // Preserve the original error and stack trace\n const wrappedError = new SyncCleanupError(this.id, error)\n wrappedError.cause = error\n wrappedError.stack = error.stack\n throw wrappedError\n } else {\n throw new SyncCleanupError(this.id, error as Error | string)\n }\n })\n }\n this.preloadPromise = null\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sync.js","sources":["../../../src/collection/sync.ts"],"sourcesContent":["import {\n CollectionIsInErrorStateError,\n DuplicateKeySyncError,\n NoPendingSyncTransactionCommitError,\n NoPendingSyncTransactionWriteError,\n SyncCleanupError,\n SyncTransactionAlreadyCommittedError,\n SyncTransactionAlreadyCommittedWriteError,\n} from \"../errors\"\nimport { deepEquals } from \"../utils\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ChangeMessage,\n CleanupFn,\n CollectionConfig,\n OnLoadMoreOptions,\n SyncConfigRes,\n} from \"../types\"\nimport type { CollectionImpl } from \"./index.js\"\nimport type { CollectionStateManager } from \"./state\"\nimport type { CollectionLifecycleManager } from \"./lifecycle\"\n\nexport class CollectionSyncManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n private config!: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n\n public preloadPromise: Promise<void> | null = null\n public syncCleanupFn: (() => void) | null = null\n public syncOnLoadMoreFn:\n | ((options: OnLoadMoreOptions) => void | Promise<void>)\n | null = null\n\n /**\n * Creates a new CollectionSyncManager instance\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.config = config\n this.id = id\n }\n\n setDeps(deps: {\n collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.collection = deps.collection\n this.state = deps.state\n this.lifecycle = deps.lifecycle\n }\n\n /**\n * Start the sync process for this collection\n * This is called when the collection is first accessed or preloaded\n */\n public startSync(): void {\n if (\n this.lifecycle.status !== `idle` &&\n this.lifecycle.status !== `cleaned-up`\n ) {\n return // Already started or in progress\n }\n\n this.lifecycle.setStatus(`loading`)\n\n try {\n const syncRes = normalizeSyncFnResult(\n this.config.sync.sync({\n collection: this.collection,\n begin: () => {\n this.state.pendingSyncedTransactions.push({\n committed: false,\n operations: [],\n deletedKeys: new Set(),\n })\n },\n write: (messageWithoutKey: Omit<ChangeMessage<TOutput>, `key`>) => {\n const pendingTransaction =\n this.state.pendingSyncedTransactions[\n this.state.pendingSyncedTransactions.length - 1\n ]\n if (!pendingTransaction) {\n throw new NoPendingSyncTransactionWriteError()\n }\n if (pendingTransaction.committed) {\n throw new SyncTransactionAlreadyCommittedWriteError()\n }\n const key = this.config.getKey(messageWithoutKey.value)\n\n let messageType = messageWithoutKey.type\n\n // Check if an item with this key already exists when inserting\n if (messageWithoutKey.type === `insert`) {\n const insertingIntoExistingSynced = this.state.syncedData.has(key)\n const hasPendingDeleteForKey =\n pendingTransaction.deletedKeys.has(key)\n const isTruncateTransaction = pendingTransaction.truncate === true\n // Allow insert after truncate in the same transaction even if it existed in syncedData\n if (\n insertingIntoExistingSynced &&\n !hasPendingDeleteForKey &&\n !isTruncateTransaction\n ) {\n const existingValue = this.state.syncedData.get(key)\n if (\n existingValue !== undefined &&\n deepEquals(existingValue, messageWithoutKey.value)\n ) {\n // The \"insert\" is an echo of a value we already have locally.\n // Treat it as an update so we preserve optimistic intent without\n // throwing a duplicate-key error during reconciliation.\n messageType = `update`\n } else {\n throw new DuplicateKeySyncError(key, this.id)\n }\n }\n }\n\n const message: ChangeMessage<TOutput> = {\n ...messageWithoutKey,\n type: messageType,\n key,\n }\n pendingTransaction.operations.push(message)\n\n if (messageType === `delete`) {\n pendingTransaction.deletedKeys.add(key)\n }\n },\n commit: () => {\n const pendingTransaction =\n this.state.pendingSyncedTransactions[\n this.state.pendingSyncedTransactions.length - 1\n ]\n if (!pendingTransaction) {\n throw new NoPendingSyncTransactionCommitError()\n }\n if (pendingTransaction.committed) {\n throw new SyncTransactionAlreadyCommittedError()\n }\n\n pendingTransaction.committed = true\n\n // Update status to initialCommit when transitioning from loading\n // This indicates we're in the process of committing the first transaction\n if (this.lifecycle.status === `loading`) {\n this.lifecycle.setStatus(`initialCommit`)\n }\n\n this.state.commitPendingTransactions()\n },\n markReady: () => {\n this.lifecycle.markReady()\n },\n truncate: () => {\n const pendingTransaction =\n this.state.pendingSyncedTransactions[\n this.state.pendingSyncedTransactions.length - 1\n ]\n if (!pendingTransaction) {\n throw new NoPendingSyncTransactionWriteError()\n }\n if (pendingTransaction.committed) {\n throw new SyncTransactionAlreadyCommittedWriteError()\n }\n\n // Clear all operations from the current transaction\n pendingTransaction.operations = []\n pendingTransaction.deletedKeys.clear()\n\n // Mark the transaction as a truncate operation. During commit, this triggers:\n // - Delete events for all previously synced keys (excluding optimistic-deleted keys)\n // - Clearing of syncedData/syncedMetadata\n // - Subsequent synced ops applied on the fresh base\n // - Finally, optimistic mutations re-applied on top (single batch)\n pendingTransaction.truncate = true\n },\n })\n )\n\n // Store cleanup function if provided\n this.syncCleanupFn = syncRes?.cleanup ?? null\n\n // Store onLoadMore function if provided\n this.syncOnLoadMoreFn = syncRes?.onLoadMore ?? null\n } catch (error) {\n this.lifecycle.setStatus(`error`)\n throw error\n }\n }\n\n /**\n * Preload the collection data by starting sync if not already started\n * Multiple concurrent calls will share the same promise\n */\n public preload(): Promise<void> {\n if (this.preloadPromise) {\n return this.preloadPromise\n }\n\n this.preloadPromise = new Promise<void>((resolve, reject) => {\n if (this.lifecycle.status === `ready`) {\n resolve()\n return\n }\n\n if (this.lifecycle.status === `error`) {\n reject(new CollectionIsInErrorStateError())\n return\n }\n\n // Register callback BEFORE starting sync to avoid race condition\n this.lifecycle.onFirstReady(() => {\n resolve()\n })\n\n // Start sync if collection hasn't started yet or was cleaned up\n if (\n this.lifecycle.status === `idle` ||\n this.lifecycle.status === `cleaned-up`\n ) {\n try {\n this.startSync()\n } catch (error) {\n reject(error)\n return\n }\n }\n })\n\n return this.preloadPromise\n }\n\n /**\n * Requests the sync layer to load more data.\n * @param options Options to control what data is being loaded\n * @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.\n * If data loading is synchronous, the data is loaded when the method returns.\n */\n public syncMore(options: OnLoadMoreOptions): void | Promise<void> {\n if (this.syncOnLoadMoreFn) {\n return this.syncOnLoadMoreFn(options)\n }\n }\n\n public cleanup(): void {\n try {\n if (this.syncCleanupFn) {\n this.syncCleanupFn()\n this.syncCleanupFn = null\n }\n } catch (error) {\n // Re-throw in a microtask to surface the error after cleanup completes\n queueMicrotask(() => {\n if (error instanceof Error) {\n // Preserve the original error and stack trace\n const wrappedError = new SyncCleanupError(this.id, error)\n wrappedError.cause = error\n wrappedError.stack = error.stack\n throw wrappedError\n } else {\n throw new SyncCleanupError(this.id, error as Error | string)\n }\n })\n }\n this.preloadPromise = null\n }\n}\n\nfunction normalizeSyncFnResult(result: void | CleanupFn | SyncConfigRes) {\n if (typeof result === `function`) {\n return { cleanup: result }\n }\n\n if (typeof result === `object`) {\n return result\n }\n\n return undefined\n}\n"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import * as IR from "./query/ir.js";
1
2
  export * from './collection/index.js';
2
3
  export * from './SortedMap.js';
3
4
  export * from './transactions.js';
@@ -13,3 +14,4 @@ export * from './indexes/btree-index.js';
13
14
  export * from './indexes/lazy-index.js';
14
15
  export { type IndexOptions } from './indexes/index-options.js';
15
16
  export type { Collection } from './collection/index.js';
17
+ export { IR };
package/dist/esm/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import * as ir from "./query/ir.js";
1
2
  import { CollectionImpl, createCollection } from "./collection/index.js";
2
3
  import { SortedMap } from "./SortedMap.js";
3
4
  import { createTransaction, getActiveTransaction } from "./transactions.js";
@@ -35,6 +36,7 @@ export {
35
36
  EmptyReferencePathError,
36
37
  GroupByError,
37
38
  HavingRequiresGroupByError,
39
+ ir as IR,
38
40
  IndexOperation,
39
41
  IndexProxy,
40
42
  InvalidCollectionStatusTransitionError,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
@@ -26,7 +26,10 @@ function ensureIndexForField(fieldName, fieldPath, collection, compareOptions =
26
26
  options: compareFn ? { compareFn, compareOptions } : {}
27
27
  });
28
28
  } catch (error) {
29
- console.warn(`Failed to create auto-index for field "${fieldName}":`, error);
29
+ console.warn(
30
+ `${collection.id ? `[${collection.id}] ` : ``}Failed to create auto-index for field "${fieldName}":`,
31
+ error
32
+ );
30
33
  }
31
34
  }
32
35
  function ensureIndexForExpression(expression, collection) {
@@ -1 +1 @@
1
- {"version":3,"file":"auto-index.js","sources":["../../../src/indexes/auto-index.ts"],"sourcesContent":["import { DEFAULT_COMPARE_OPTIONS } from \"../utils\"\nimport { BTreeIndex } from \"./btree-index\"\nimport type { CompareOptions } from \"../query/builder/types\"\nimport type { BasicExpression } from \"../query/ir\"\nimport type { CollectionImpl } from \"../collection/index.js\"\n\nexport interface AutoIndexConfig {\n autoIndex?: `off` | `eager`\n}\n\nfunction shouldAutoIndex(collection: CollectionImpl<any, any, any, any, any>) {\n // Only proceed if auto-indexing is enabled\n if (collection.config.autoIndex !== `eager`) {\n return false\n }\n\n // Don't auto-index during sync operations\n if (\n collection.status === `loading` ||\n collection.status === `initialCommit`\n ) {\n return false\n }\n\n return true\n}\n\nexport function ensureIndexForField<\n T extends Record<string, any>,\n TKey extends string | number,\n>(\n fieldName: string,\n fieldPath: Array<string>,\n collection: CollectionImpl<T, TKey, any, any, any>,\n compareOptions: CompareOptions = DEFAULT_COMPARE_OPTIONS,\n compareFn?: (a: any, b: any) => number\n) {\n if (!shouldAutoIndex(collection)) {\n return\n }\n\n // Check if we already have an index for this field\n const existingIndex = Array.from(collection.indexes.values()).find(\n (index) =>\n index.matchesField(fieldPath) &&\n index.matchesCompareOptions(compareOptions)\n )\n\n if (existingIndex) {\n return // Index already exists\n }\n\n // Create a new index for this field using the collection's createIndex method\n try {\n collection.createIndex((row) => (row as any)[fieldName], {\n name: `auto_${fieldName}`,\n indexType: BTreeIndex,\n options: compareFn ? { compareFn, compareOptions } : {},\n })\n } catch (error) {\n console.warn(`Failed to create auto-index for field \"${fieldName}\":`, error)\n }\n}\n\n/**\n * Analyzes a where expression and creates indexes for all simple operations on single fields\n */\nexport function ensureIndexForExpression<\n T extends Record<string, any>,\n TKey extends string | number,\n>(\n expression: BasicExpression,\n collection: CollectionImpl<T, TKey, any, any, any>\n): void {\n if (!shouldAutoIndex(collection)) {\n return\n }\n\n // Extract all indexable expressions and create indexes for them\n const indexableExpressions = extractIndexableExpressions(expression)\n\n for (const { fieldName, fieldPath } of indexableExpressions) {\n ensureIndexForField(fieldName, fieldPath, collection)\n }\n}\n\n/**\n * Extracts all indexable expressions from a where expression\n */\nfunction extractIndexableExpressions(\n expression: BasicExpression\n): Array<{ fieldName: string; fieldPath: Array<string> }> {\n const results: Array<{ fieldName: string; fieldPath: Array<string> }> = []\n\n function extractFromExpression(expr: BasicExpression): void {\n if (expr.type !== `func`) {\n return\n }\n\n const func = expr as any\n\n // Handle 'and' expressions by recursively processing all arguments\n if (func.name === `and`) {\n for (const arg of func.args) {\n extractFromExpression(arg)\n }\n return\n }\n\n // Check if this is a supported operation\n const supportedOperations = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]\n if (!supportedOperations.includes(func.name)) {\n return\n }\n\n // Check if the first argument is a property reference (single field)\n if (func.args.length < 1 || func.args[0].type !== `ref`) {\n return\n }\n\n const fieldRef = func.args[0]\n const fieldPath = fieldRef.path\n\n // Skip if it's not a simple field (e.g., nested properties or array access)\n if (fieldPath.length !== 1) {\n return\n }\n\n const fieldName = fieldPath[0]\n results.push({ fieldName, fieldPath })\n }\n\n extractFromExpression(expression)\n return results\n}\n"],"names":[],"mappings":";;AAUA,SAAS,gBAAgB,YAAqD;AAE5E,MAAI,WAAW,OAAO,cAAc,SAAS;AAC3C,WAAO;AAAA,EACT;AAGA,MACE,WAAW,WAAW,aACtB,WAAW,WAAW,iBACtB;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEO,SAAS,oBAId,WACA,WACA,YACA,iBAAiC,yBACjC,WACA;AACA,MAAI,CAAC,gBAAgB,UAAU,GAAG;AAChC;AAAA,EACF;AAGA,QAAM,gBAAgB,MAAM,KAAK,WAAW,QAAQ,OAAA,CAAQ,EAAE;AAAA,IAC5D,CAAC,UACC,MAAM,aAAa,SAAS,KAC5B,MAAM,sBAAsB,cAAc;AAAA,EAAA;AAG9C,MAAI,eAAe;AACjB;AAAA,EACF;AAGA,MAAI;AACF,eAAW,YAAY,CAAC,QAAS,IAAY,SAAS,GAAG;AAAA,MACvD,MAAM,QAAQ,SAAS;AAAA,MACvB,WAAW;AAAA,MACX,SAAS,YAAY,EAAE,WAAW,eAAA,IAAmB,CAAA;AAAA,IAAC,CACvD;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,KAAK,0CAA0C,SAAS,MAAM,KAAK;AAAA,EAC7E;AACF;AAKO,SAAS,yBAId,YACA,YACM;AACN,MAAI,CAAC,gBAAgB,UAAU,GAAG;AAChC;AAAA,EACF;AAGA,QAAM,uBAAuB,4BAA4B,UAAU;AAEnE,aAAW,EAAE,WAAW,UAAA,KAAe,sBAAsB;AAC3D,wBAAoB,WAAW,WAAW,UAAU;AAAA,EACtD;AACF;AAKA,SAAS,4BACP,YACwD;AACxD,QAAM,UAAkE,CAAA;AAExE,WAAS,sBAAsB,MAA6B;AAC1D,QAAI,KAAK,SAAS,QAAQ;AACxB;AAAA,IACF;AAEA,UAAM,OAAO;AAGb,QAAI,KAAK,SAAS,OAAO;AACvB,iBAAW,OAAO,KAAK,MAAM;AAC3B,8BAAsB,GAAG;AAAA,MAC3B;AACA;AAAA,IACF;AAGA,UAAM,sBAAsB,CAAC,MAAM,MAAM,OAAO,MAAM,OAAO,IAAI;AACjE,QAAI,CAAC,oBAAoB,SAAS,KAAK,IAAI,GAAG;AAC5C;AAAA,IACF;AAGA,QAAI,KAAK,KAAK,SAAS,KAAK,KAAK,KAAK,CAAC,EAAE,SAAS,OAAO;AACvD;AAAA,IACF;AAEA,UAAM,WAAW,KAAK,KAAK,CAAC;AAC5B,UAAM,YAAY,SAAS;AAG3B,QAAI,UAAU,WAAW,GAAG;AAC1B;AAAA,IACF;AAEA,UAAM,YAAY,UAAU,CAAC;AAC7B,YAAQ,KAAK,EAAE,WAAW,UAAA,CAAW;AAAA,EACvC;AAEA,wBAAsB,UAAU;AAChC,SAAO;AACT;"}
1
+ {"version":3,"file":"auto-index.js","sources":["../../../src/indexes/auto-index.ts"],"sourcesContent":["import { DEFAULT_COMPARE_OPTIONS } from \"../utils\"\nimport { BTreeIndex } from \"./btree-index\"\nimport type { CompareOptions } from \"../query/builder/types\"\nimport type { BasicExpression } from \"../query/ir\"\nimport type { CollectionImpl } from \"../collection/index.js\"\n\nexport interface AutoIndexConfig {\n autoIndex?: `off` | `eager`\n}\n\nfunction shouldAutoIndex(collection: CollectionImpl<any, any, any, any, any>) {\n // Only proceed if auto-indexing is enabled\n if (collection.config.autoIndex !== `eager`) {\n return false\n }\n\n // Don't auto-index during sync operations\n if (\n collection.status === `loading` ||\n collection.status === `initialCommit`\n ) {\n return false\n }\n\n return true\n}\n\nexport function ensureIndexForField<\n T extends Record<string, any>,\n TKey extends string | number,\n>(\n fieldName: string,\n fieldPath: Array<string>,\n collection: CollectionImpl<T, TKey, any, any, any>,\n compareOptions: CompareOptions = DEFAULT_COMPARE_OPTIONS,\n compareFn?: (a: any, b: any) => number\n) {\n if (!shouldAutoIndex(collection)) {\n return\n }\n\n // Check if we already have an index for this field\n const existingIndex = Array.from(collection.indexes.values()).find(\n (index) =>\n index.matchesField(fieldPath) &&\n index.matchesCompareOptions(compareOptions)\n )\n\n if (existingIndex) {\n return // Index already exists\n }\n\n // Create a new index for this field using the collection's createIndex method\n try {\n collection.createIndex((row) => (row as any)[fieldName], {\n name: `auto_${fieldName}`,\n indexType: BTreeIndex,\n options: compareFn ? { compareFn, compareOptions } : {},\n })\n } catch (error) {\n console.warn(\n `${collection.id ? `[${collection.id}] ` : ``}Failed to create auto-index for field \"${fieldName}\":`,\n error\n )\n }\n}\n\n/**\n * Analyzes a where expression and creates indexes for all simple operations on single fields\n */\nexport function ensureIndexForExpression<\n T extends Record<string, any>,\n TKey extends string | number,\n>(\n expression: BasicExpression,\n collection: CollectionImpl<T, TKey, any, any, any>\n): void {\n if (!shouldAutoIndex(collection)) {\n return\n }\n\n // Extract all indexable expressions and create indexes for them\n const indexableExpressions = extractIndexableExpressions(expression)\n\n for (const { fieldName, fieldPath } of indexableExpressions) {\n ensureIndexForField(fieldName, fieldPath, collection)\n }\n}\n\n/**\n * Extracts all indexable expressions from a where expression\n */\nfunction extractIndexableExpressions(\n expression: BasicExpression\n): Array<{ fieldName: string; fieldPath: Array<string> }> {\n const results: Array<{ fieldName: string; fieldPath: Array<string> }> = []\n\n function extractFromExpression(expr: BasicExpression): void {\n if (expr.type !== `func`) {\n return\n }\n\n const func = expr as any\n\n // Handle 'and' expressions by recursively processing all arguments\n if (func.name === `and`) {\n for (const arg of func.args) {\n extractFromExpression(arg)\n }\n return\n }\n\n // Check if this is a supported operation\n const supportedOperations = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]\n if (!supportedOperations.includes(func.name)) {\n return\n }\n\n // Check if the first argument is a property reference (single field)\n if (func.args.length < 1 || func.args[0].type !== `ref`) {\n return\n }\n\n const fieldRef = func.args[0]\n const fieldPath = fieldRef.path\n\n // Skip if it's not a simple field (e.g., nested properties or array access)\n if (fieldPath.length !== 1) {\n return\n }\n\n const fieldName = fieldPath[0]\n results.push({ fieldName, fieldPath })\n }\n\n extractFromExpression(expression)\n return results\n}\n"],"names":[],"mappings":";;AAUA,SAAS,gBAAgB,YAAqD;AAE5E,MAAI,WAAW,OAAO,cAAc,SAAS;AAC3C,WAAO;AAAA,EACT;AAGA,MACE,WAAW,WAAW,aACtB,WAAW,WAAW,iBACtB;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEO,SAAS,oBAId,WACA,WACA,YACA,iBAAiC,yBACjC,WACA;AACA,MAAI,CAAC,gBAAgB,UAAU,GAAG;AAChC;AAAA,EACF;AAGA,QAAM,gBAAgB,MAAM,KAAK,WAAW,QAAQ,OAAA,CAAQ,EAAE;AAAA,IAC5D,CAAC,UACC,MAAM,aAAa,SAAS,KAC5B,MAAM,sBAAsB,cAAc;AAAA,EAAA;AAG9C,MAAI,eAAe;AACjB;AAAA,EACF;AAGA,MAAI;AACF,eAAW,YAAY,CAAC,QAAS,IAAY,SAAS,GAAG;AAAA,MACvD,MAAM,QAAQ,SAAS;AAAA,MACvB,WAAW;AAAA,MACX,SAAS,YAAY,EAAE,WAAW,eAAA,IAAmB,CAAA;AAAA,IAAC,CACvD;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN,GAAG,WAAW,KAAK,IAAI,WAAW,EAAE,OAAO,EAAE,0CAA0C,SAAS;AAAA,MAChG;AAAA,IAAA;AAAA,EAEJ;AACF;AAKO,SAAS,yBAId,YACA,YACM;AACN,MAAI,CAAC,gBAAgB,UAAU,GAAG;AAChC;AAAA,EACF;AAGA,QAAM,uBAAuB,4BAA4B,UAAU;AAEnE,aAAW,EAAE,WAAW,UAAA,KAAe,sBAAsB;AAC3D,wBAAoB,WAAW,WAAW,UAAU;AAAA,EACtD;AACF;AAKA,SAAS,4BACP,YACwD;AACxD,QAAM,UAAkE,CAAA;AAExE,WAAS,sBAAsB,MAA6B;AAC1D,QAAI,KAAK,SAAS,QAAQ;AACxB;AAAA,IACF;AAEA,UAAM,OAAO;AAGb,QAAI,KAAK,SAAS,OAAO;AACvB,iBAAW,OAAO,KAAK,MAAM;AAC3B,8BAAsB,GAAG;AAAA,MAC3B;AACA;AAAA,IACF;AAGA,UAAM,sBAAsB,CAAC,MAAM,MAAM,OAAO,MAAM,OAAO,IAAI;AACjE,QAAI,CAAC,oBAAoB,SAAS,KAAK,IAAI,GAAG;AAC5C;AAAA,IACF;AAGA,QAAI,KAAK,KAAK,SAAS,KAAK,KAAK,KAAK,CAAC,EAAE,SAAS,OAAO;AACvD;AAAA,IACF;AAEA,UAAM,WAAW,KAAK,KAAK,CAAC;AAC5B,UAAM,YAAY,SAAS;AAG3B,QAAI,UAAU,WAAW,GAAG;AAC1B;AAAA,IACF;AAEA,UAAM,YAAY,UAAU,CAAC;AAC7B,YAAQ,KAAK,EAAE,WAAW,UAAA,CAAW;AAAA,EACvC;AAEA,wBAAsB,UAAU;AAChC,SAAO;AACT;"}
@@ -1,4 +1,4 @@
1
- import { BasicExpression } from '../ir.js';
1
+ import { BasicExpression, OrderBy } from '../ir.js';
2
2
  /**
3
3
  * Functions supported by the collection index system.
4
4
  * These are the only functions that can be used in WHERE clauses
@@ -23,3 +23,4 @@ export declare function isConvertibleToCollectionFilter(whereClause: BasicExpres
23
23
  * @returns The converted BasicExpression or null if conversion fails
24
24
  */
25
25
  export declare function convertToBasicExpression(whereClause: BasicExpression<boolean>, collectionAlias: string): BasicExpression<boolean> | null;
26
+ export declare function convertOrderByToBasicExpression(orderBy: OrderBy, collectionAlias: string): OrderBy;
@@ -53,8 +53,27 @@ function convertToBasicExpression(whereClause, collectionAlias) {
53
53
  return new Func(whereClause.name, args);
54
54
  }
55
55
  }
56
+ function convertOrderByToBasicExpression(orderBy, collectionAlias) {
57
+ const normalizedOrderBy = orderBy.map((clause) => {
58
+ const basicExp = convertToBasicExpression(
59
+ clause.expression,
60
+ collectionAlias
61
+ );
62
+ if (!basicExp) {
63
+ throw new Error(
64
+ `Failed to convert orderBy expression to a basic expression: ${clause.expression}`
65
+ );
66
+ }
67
+ return {
68
+ ...clause,
69
+ expression: basicExp
70
+ };
71
+ });
72
+ return normalizedOrderBy;
73
+ }
56
74
  export {
57
75
  SUPPORTED_COLLECTION_FUNCS,
76
+ convertOrderByToBasicExpression,
58
77
  convertToBasicExpression,
59
78
  isConvertibleToCollectionFilter
60
79
  };
@@ -1 +1 @@
1
- {"version":3,"file":"expressions.js","sources":["../../../../src/query/compiler/expressions.ts"],"sourcesContent":["import { Func, PropRef, Value } from \"../ir.js\"\nimport type { BasicExpression } from \"../ir.js\"\n\n/**\n * Functions supported by the collection index system.\n * These are the only functions that can be used in WHERE clauses\n * that are pushed down to collection subscriptions for index optimization.\n */\nexport const SUPPORTED_COLLECTION_FUNCS = new Set([\n `eq`,\n `gt`,\n `lt`,\n `gte`,\n `lte`,\n `and`,\n `or`,\n `in`,\n])\n\n/**\n * Determines if a WHERE clause can be converted to collection-compatible BasicExpression format.\n * This checks if the expression only uses functions supported by the collection index system.\n *\n * @param whereClause - The WHERE clause to check\n * @returns True if the clause can be converted for collection index optimization\n */\nexport function isConvertibleToCollectionFilter(\n whereClause: BasicExpression<boolean>\n): boolean {\n const tpe = whereClause.type\n if (tpe === `func`) {\n // Check if this function is supported\n if (!SUPPORTED_COLLECTION_FUNCS.has(whereClause.name)) {\n return false\n }\n // Recursively check all arguments\n return whereClause.args.every((arg) =>\n isConvertibleToCollectionFilter(arg as BasicExpression<boolean>)\n )\n }\n return [`val`, `ref`].includes(tpe)\n}\n\n/**\n * Converts a WHERE clause to BasicExpression format compatible with collection indexes.\n * This function creates proper BasicExpression class instances that the collection\n * index system can understand.\n *\n * @param whereClause - The WHERE clause to convert\n * @param collectionAlias - The alias of the collection being filtered\n * @returns The converted BasicExpression or null if conversion fails\n */\nexport function convertToBasicExpression(\n whereClause: BasicExpression<boolean>,\n collectionAlias: string\n): BasicExpression<boolean> | null {\n const tpe = whereClause.type\n if (tpe === `val`) {\n return new Value(whereClause.value)\n } else if (tpe === `ref`) {\n const path = whereClause.path\n if (Array.isArray(path)) {\n if (path[0] === collectionAlias && path.length > 1) {\n // Remove the table alias from the path for single-collection queries\n return new PropRef(path.slice(1))\n } else if (path.length === 1 && path[0] !== undefined) {\n // Single field reference\n return new PropRef([path[0]])\n }\n }\n // Fallback for non-array paths\n return new PropRef(Array.isArray(path) ? path : [String(path)])\n } else {\n // Check if this function is supported\n if (!SUPPORTED_COLLECTION_FUNCS.has(whereClause.name)) {\n return null\n }\n // Recursively convert all arguments\n const args: Array<BasicExpression> = []\n for (const arg of whereClause.args) {\n const convertedArg = convertToBasicExpression(\n arg as BasicExpression<boolean>,\n collectionAlias\n )\n if (convertedArg == null) {\n return null\n }\n args.push(convertedArg)\n }\n return new Func(whereClause.name, args)\n }\n}\n"],"names":[],"mappings":";AAQO,MAAM,iDAAiC,IAAI;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AASM,SAAS,gCACd,aACS;AACT,QAAM,MAAM,YAAY;AACxB,MAAI,QAAQ,QAAQ;AAElB,QAAI,CAAC,2BAA2B,IAAI,YAAY,IAAI,GAAG;AACrD,aAAO;AAAA,IACT;AAEA,WAAO,YAAY,KAAK;AAAA,MAAM,CAAC,QAC7B,gCAAgC,GAA+B;AAAA,IAAA;AAAA,EAEnE;AACA,SAAO,CAAC,OAAO,KAAK,EAAE,SAAS,GAAG;AACpC;AAWO,SAAS,yBACd,aACA,iBACiC;AACjC,QAAM,MAAM,YAAY;AACxB,MAAI,QAAQ,OAAO;AACjB,WAAO,IAAI,MAAM,YAAY,KAAK;AAAA,EACpC,WAAW,QAAQ,OAAO;AACxB,UAAM,OAAO,YAAY;AACzB,QAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,UAAI,KAAK,CAAC,MAAM,mBAAmB,KAAK,SAAS,GAAG;AAElD,eAAO,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC;AAAA,MAClC,WAAW,KAAK,WAAW,KAAK,KAAK,CAAC,MAAM,QAAW;AAErD,eAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AAAA,MAC9B;AAAA,IACF;AAEA,WAAO,IAAI,QAAQ,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;AAAA,EAChE,OAAO;AAEL,QAAI,CAAC,2BAA2B,IAAI,YAAY,IAAI,GAAG;AACrD,aAAO;AAAA,IACT;AAEA,UAAM,OAA+B,CAAA;AACrC,eAAW,OAAO,YAAY,MAAM;AAClC,YAAM,eAAe;AAAA,QACnB;AAAA,QACA;AAAA,MAAA;AAEF,UAAI,gBAAgB,MAAM;AACxB,eAAO;AAAA,MACT;AACA,WAAK,KAAK,YAAY;AAAA,IACxB;AACA,WAAO,IAAI,KAAK,YAAY,MAAM,IAAI;AAAA,EACxC;AACF;"}
1
+ {"version":3,"file":"expressions.js","sources":["../../../../src/query/compiler/expressions.ts"],"sourcesContent":["import { Func, PropRef, Value } from \"../ir.js\"\nimport type { BasicExpression, OrderBy } from \"../ir.js\"\n\n/**\n * Functions supported by the collection index system.\n * These are the only functions that can be used in WHERE clauses\n * that are pushed down to collection subscriptions for index optimization.\n */\nexport const SUPPORTED_COLLECTION_FUNCS = new Set([\n `eq`,\n `gt`,\n `lt`,\n `gte`,\n `lte`,\n `and`,\n `or`,\n `in`,\n])\n\n/**\n * Determines if a WHERE clause can be converted to collection-compatible BasicExpression format.\n * This checks if the expression only uses functions supported by the collection index system.\n *\n * @param whereClause - The WHERE clause to check\n * @returns True if the clause can be converted for collection index optimization\n */\nexport function isConvertibleToCollectionFilter(\n whereClause: BasicExpression<boolean>\n): boolean {\n const tpe = whereClause.type\n if (tpe === `func`) {\n // Check if this function is supported\n if (!SUPPORTED_COLLECTION_FUNCS.has(whereClause.name)) {\n return false\n }\n // Recursively check all arguments\n return whereClause.args.every((arg) =>\n isConvertibleToCollectionFilter(arg as BasicExpression<boolean>)\n )\n }\n return [`val`, `ref`].includes(tpe)\n}\n\n/**\n * Converts a WHERE clause to BasicExpression format compatible with collection indexes.\n * This function creates proper BasicExpression class instances that the collection\n * index system can understand.\n *\n * @param whereClause - The WHERE clause to convert\n * @param collectionAlias - The alias of the collection being filtered\n * @returns The converted BasicExpression or null if conversion fails\n */\nexport function convertToBasicExpression(\n whereClause: BasicExpression<boolean>,\n collectionAlias: string\n): BasicExpression<boolean> | null {\n const tpe = whereClause.type\n if (tpe === `val`) {\n return new Value(whereClause.value)\n } else if (tpe === `ref`) {\n const path = whereClause.path\n if (Array.isArray(path)) {\n if (path[0] === collectionAlias && path.length > 1) {\n // Remove the table alias from the path for single-collection queries\n return new PropRef(path.slice(1))\n } else if (path.length === 1 && path[0] !== undefined) {\n // Single field reference\n return new PropRef([path[0]])\n }\n }\n // Fallback for non-array paths\n return new PropRef(Array.isArray(path) ? path : [String(path)])\n } else {\n // Check if this function is supported\n if (!SUPPORTED_COLLECTION_FUNCS.has(whereClause.name)) {\n return null\n }\n // Recursively convert all arguments\n const args: Array<BasicExpression> = []\n for (const arg of whereClause.args) {\n const convertedArg = convertToBasicExpression(\n arg as BasicExpression<boolean>,\n collectionAlias\n )\n if (convertedArg == null) {\n return null\n }\n args.push(convertedArg)\n }\n return new Func(whereClause.name, args)\n }\n}\n\nexport function convertOrderByToBasicExpression(\n orderBy: OrderBy,\n collectionAlias: string\n): OrderBy {\n const normalizedOrderBy = orderBy.map((clause) => {\n const basicExp = convertToBasicExpression(\n clause.expression,\n collectionAlias\n )\n\n if (!basicExp) {\n throw new Error(\n `Failed to convert orderBy expression to a basic expression: ${clause.expression}`\n )\n }\n\n return {\n ...clause,\n expression: basicExp,\n }\n })\n\n return normalizedOrderBy\n}\n"],"names":[],"mappings":";AAQO,MAAM,iDAAiC,IAAI;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AASM,SAAS,gCACd,aACS;AACT,QAAM,MAAM,YAAY;AACxB,MAAI,QAAQ,QAAQ;AAElB,QAAI,CAAC,2BAA2B,IAAI,YAAY,IAAI,GAAG;AACrD,aAAO;AAAA,IACT;AAEA,WAAO,YAAY,KAAK;AAAA,MAAM,CAAC,QAC7B,gCAAgC,GAA+B;AAAA,IAAA;AAAA,EAEnE;AACA,SAAO,CAAC,OAAO,KAAK,EAAE,SAAS,GAAG;AACpC;AAWO,SAAS,yBACd,aACA,iBACiC;AACjC,QAAM,MAAM,YAAY;AACxB,MAAI,QAAQ,OAAO;AACjB,WAAO,IAAI,MAAM,YAAY,KAAK;AAAA,EACpC,WAAW,QAAQ,OAAO;AACxB,UAAM,OAAO,YAAY;AACzB,QAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,UAAI,KAAK,CAAC,MAAM,mBAAmB,KAAK,SAAS,GAAG;AAElD,eAAO,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC;AAAA,MAClC,WAAW,KAAK,WAAW,KAAK,KAAK,CAAC,MAAM,QAAW;AAErD,eAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AAAA,MAC9B;AAAA,IACF;AAEA,WAAO,IAAI,QAAQ,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;AAAA,EAChE,OAAO;AAEL,QAAI,CAAC,2BAA2B,IAAI,YAAY,IAAI,GAAG;AACrD,aAAO;AAAA,IACT;AAEA,UAAM,OAA+B,CAAA;AACrC,eAAW,OAAO,YAAY,MAAM;AAClC,YAAM,eAAe;AAAA,QACnB;AAAA,QACA;AAAA,MAAA;AAEF,UAAI,gBAAgB,MAAM;AACxB,eAAO;AAAA,MACT;AACA,WAAK,KAAK,YAAY;AAAA,IACxB;AACA,WAAO,IAAI,KAAK,YAAY,MAAM,IAAI;AAAA,EACxC;AACF;AAEO,SAAS,gCACd,SACA,iBACS;AACT,QAAM,oBAAoB,QAAQ,IAAI,CAAC,WAAW;AAChD,UAAM,WAAW;AAAA,MACf,OAAO;AAAA,MACP;AAAA,IAAA;AAGF,QAAI,CAAC,UAAU;AACb,YAAM,IAAI;AAAA,QACR,+DAA+D,OAAO,UAAU;AAAA,MAAA;AAAA,IAEpF;AAEA,WAAO;AAAA,MACL,GAAG;AAAA,MACH,YAAY;AAAA,IAAA;AAAA,EAEhB,CAAC;AAED,SAAO;AACT;"}
@@ -1,9 +1,10 @@
1
- import { OrderByClause, QueryIR, Select } from '../ir.js';
1
+ import { OrderBy, OrderByClause, QueryIR, Select } from '../ir.js';
2
2
  import { NamespacedAndKeyedStream, NamespacedRow } from '../../types.js';
3
3
  import { IStreamBuilder, KeyValue } from '@tanstack/db-ivm';
4
4
  import { IndexInterface } from '../../indexes/base-index.js';
5
5
  import { Collection } from '../../collection/index.js';
6
6
  export type OrderByOptimizationInfo = {
7
+ orderBy: OrderBy;
7
8
  offset: number;
8
9
  limit: number;
9
10
  comparator: (a: Record<string, unknown> | null | undefined, b: Record<string, unknown> | null | undefined) => number;
@@ -91,7 +91,8 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
91
91
  limit,
92
92
  comparator,
93
93
  valueExtractorForRawRow,
94
- index
94
+ index,
95
+ orderBy: orderByClause
95
96
  };
96
97
  optimizableOrderByCollections[followRefCollection.id] = orderByOptimizationInfo;
97
98
  setSizeCallback = (getSize) => {
@@ -1 +1 @@
1
- {"version":3,"file":"order-by.js","sources":["../../../../src/query/compiler/order-by.ts"],"sourcesContent":["import { orderByWithFractionalIndex } from \"@tanstack/db-ivm\"\nimport { defaultComparator, makeComparator } from \"../../utils/comparison.js\"\nimport { PropRef, followRef } from \"../ir.js\"\nimport { ensureIndexForField } from \"../../indexes/auto-index.js\"\nimport { findIndexForField } from \"../../utils/index-optimization.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { replaceAggregatesByRefs } from \"./group-by.js\"\nimport type { CompiledSingleRowExpression } from \"./evaluators.js\"\nimport type { OrderByClause, QueryIR, Select } from \"../ir.js\"\nimport type { NamespacedAndKeyedStream, NamespacedRow } from \"../../types.js\"\nimport type { IStreamBuilder, KeyValue } from \"@tanstack/db-ivm\"\nimport type { IndexInterface } from \"../../indexes/base-index.js\"\nimport type { Collection } from \"../../collection/index.js\"\n\nexport type OrderByOptimizationInfo = {\n offset: number\n limit: number\n comparator: (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => number\n valueExtractorForRawRow: (row: Record<string, unknown>) => any\n index: IndexInterface<string | number>\n dataNeeded?: () => number\n}\n\n/**\n * Processes the ORDER BY clause\n * Works with the new structure that has both namespaced row data and __select_results\n * Always uses fractional indexing and adds the index as __ordering_index to the result\n */\nexport function processOrderBy(\n rawQuery: QueryIR,\n pipeline: NamespacedAndKeyedStream,\n orderByClause: Array<OrderByClause>,\n selectClause: Select,\n collection: Collection,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n limit?: number,\n offset?: number\n): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {\n // Pre-compile all order by expressions\n const compiledOrderBy = orderByClause.map((clause) => {\n const clauseWithoutAggregates = replaceAggregatesByRefs(\n clause.expression,\n selectClause,\n `__select_results`\n )\n return {\n compiledExpression: compileExpression(clauseWithoutAggregates),\n compareOptions: clause.compareOptions,\n }\n })\n\n // Create a value extractor function for the orderBy operator\n const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {\n // The namespaced row contains:\n // 1. Table aliases as top-level properties (e.g., row[\"tableName\"])\n // 2. SELECT results in __select_results (e.g., row.__select_results[\"aggregateAlias\"])\n // The replaceAggregatesByRefs function has already transformed any aggregate expressions\n // that match SELECT aggregates to use the __select_results namespace.\n const orderByContext = row\n\n if (orderByClause.length > 1) {\n // For multiple orderBy columns, create a composite key\n return compiledOrderBy.map((compiled) =>\n compiled.compiledExpression(orderByContext)\n )\n } else if (orderByClause.length === 1) {\n // For a single orderBy column, use the value directly\n const compiled = compiledOrderBy[0]!\n return compiled.compiledExpression(orderByContext)\n }\n\n // Default case - no ordering\n return null\n }\n\n // Create a multi-property comparator that respects the order and direction of each property\n const compare = (a: unknown, b: unknown) => {\n // If we're comparing arrays (multiple properties), compare each property in order\n if (orderByClause.length > 1) {\n const arrayA = a as Array<unknown>\n const arrayB = b as Array<unknown>\n for (let i = 0; i < orderByClause.length; i++) {\n const clause = orderByClause[i]!\n const compareFn = makeComparator(clause.compareOptions)\n const result = compareFn(arrayA[i], arrayB[i])\n if (result !== 0) {\n return result\n }\n }\n return arrayA.length - arrayB.length\n }\n\n // Single property comparison\n if (orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const compareFn = makeComparator(clause.compareOptions)\n return compareFn(a, b)\n }\n\n return defaultComparator(a, b)\n }\n\n let setSizeCallback: ((getSize: () => number) => void) | undefined\n\n // Optimize the orderBy operator to lazily load elements\n // by using the range index of the collection.\n // Only for orderBy clause on a single column for now (no composite ordering)\n if (limit && orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const orderByExpression = clause.expression\n\n if (orderByExpression.type === `ref`) {\n const followRefResult = followRef(\n rawQuery,\n orderByExpression,\n collection\n )!\n\n const followRefCollection = followRefResult.collection\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n clause.compareOptions,\n compare\n )\n }\n\n const valueExtractorForRawRow = compileExpression(\n new PropRef(followRefResult.path),\n true\n ) as CompiledSingleRowExpression\n\n const comparator = (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => {\n const extractedA = a ? valueExtractorForRawRow(a) : a\n const extractedB = b ? valueExtractorForRawRow(b) : b\n return compare(extractedA, extractedB)\n }\n\n const index: IndexInterface<string | number> | undefined =\n findIndexForField(\n followRefCollection.indexes,\n followRefResult.path,\n clause.compareOptions\n )\n\n if (index && index.supports(`gt`)) {\n // We found an index that we can use to lazily load ordered data\n const orderByOptimizationInfo = {\n offset: offset ?? 0,\n limit,\n comparator,\n valueExtractorForRawRow,\n index,\n }\n\n optimizableOrderByCollections[followRefCollection.id] =\n orderByOptimizationInfo\n\n setSizeCallback = (getSize: () => number) => {\n optimizableOrderByCollections[followRefCollection.id] = {\n ...optimizableOrderByCollections[followRefCollection.id]!,\n dataNeeded: () => {\n const size = getSize()\n return Math.max(0, limit - size)\n },\n }\n }\n }\n }\n }\n\n // Use fractional indexing and return the tuple [value, index]\n return pipeline.pipe(\n orderByWithFractionalIndex(valueExtractor, {\n limit,\n offset,\n comparator: compare,\n setSizeCallback,\n })\n // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"order-by.js","sources":["../../../../src/query/compiler/order-by.ts"],"sourcesContent":["import { orderByWithFractionalIndex } from \"@tanstack/db-ivm\"\nimport { defaultComparator, makeComparator } from \"../../utils/comparison.js\"\nimport { PropRef, followRef } from \"../ir.js\"\nimport { ensureIndexForField } from \"../../indexes/auto-index.js\"\nimport { findIndexForField } from \"../../utils/index-optimization.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { replaceAggregatesByRefs } from \"./group-by.js\"\nimport type { CompiledSingleRowExpression } from \"./evaluators.js\"\nimport type { OrderBy, OrderByClause, QueryIR, Select } from \"../ir.js\"\nimport type { NamespacedAndKeyedStream, NamespacedRow } from \"../../types.js\"\nimport type { IStreamBuilder, KeyValue } from \"@tanstack/db-ivm\"\nimport type { IndexInterface } from \"../../indexes/base-index.js\"\nimport type { Collection } from \"../../collection/index.js\"\n\nexport type OrderByOptimizationInfo = {\n orderBy: OrderBy\n offset: number\n limit: number\n comparator: (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => number\n valueExtractorForRawRow: (row: Record<string, unknown>) => any\n index: IndexInterface<string | number>\n dataNeeded?: () => number\n}\n\n/**\n * Processes the ORDER BY clause\n * Works with the new structure that has both namespaced row data and __select_results\n * Always uses fractional indexing and adds the index as __ordering_index to the result\n */\nexport function processOrderBy(\n rawQuery: QueryIR,\n pipeline: NamespacedAndKeyedStream,\n orderByClause: Array<OrderByClause>,\n selectClause: Select,\n collection: Collection,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n limit?: number,\n offset?: number\n): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {\n // Pre-compile all order by expressions\n const compiledOrderBy = orderByClause.map((clause) => {\n const clauseWithoutAggregates = replaceAggregatesByRefs(\n clause.expression,\n selectClause,\n `__select_results`\n )\n return {\n compiledExpression: compileExpression(clauseWithoutAggregates),\n compareOptions: clause.compareOptions,\n }\n })\n\n // Create a value extractor function for the orderBy operator\n const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {\n // The namespaced row contains:\n // 1. Table aliases as top-level properties (e.g., row[\"tableName\"])\n // 2. SELECT results in __select_results (e.g., row.__select_results[\"aggregateAlias\"])\n // The replaceAggregatesByRefs function has already transformed any aggregate expressions\n // that match SELECT aggregates to use the __select_results namespace.\n const orderByContext = row\n\n if (orderByClause.length > 1) {\n // For multiple orderBy columns, create a composite key\n return compiledOrderBy.map((compiled) =>\n compiled.compiledExpression(orderByContext)\n )\n } else if (orderByClause.length === 1) {\n // For a single orderBy column, use the value directly\n const compiled = compiledOrderBy[0]!\n return compiled.compiledExpression(orderByContext)\n }\n\n // Default case - no ordering\n return null\n }\n\n // Create a multi-property comparator that respects the order and direction of each property\n const compare = (a: unknown, b: unknown) => {\n // If we're comparing arrays (multiple properties), compare each property in order\n if (orderByClause.length > 1) {\n const arrayA = a as Array<unknown>\n const arrayB = b as Array<unknown>\n for (let i = 0; i < orderByClause.length; i++) {\n const clause = orderByClause[i]!\n const compareFn = makeComparator(clause.compareOptions)\n const result = compareFn(arrayA[i], arrayB[i])\n if (result !== 0) {\n return result\n }\n }\n return arrayA.length - arrayB.length\n }\n\n // Single property comparison\n if (orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const compareFn = makeComparator(clause.compareOptions)\n return compareFn(a, b)\n }\n\n return defaultComparator(a, b)\n }\n\n let setSizeCallback: ((getSize: () => number) => void) | undefined\n\n // Optimize the orderBy operator to lazily load elements\n // by using the range index of the collection.\n // Only for orderBy clause on a single column for now (no composite ordering)\n if (limit && orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const orderByExpression = clause.expression\n\n if (orderByExpression.type === `ref`) {\n const followRefResult = followRef(\n rawQuery,\n orderByExpression,\n collection\n )!\n\n const followRefCollection = followRefResult.collection\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n clause.compareOptions,\n compare\n )\n }\n\n const valueExtractorForRawRow = compileExpression(\n new PropRef(followRefResult.path),\n true\n ) as CompiledSingleRowExpression\n\n const comparator = (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => {\n const extractedA = a ? valueExtractorForRawRow(a) : a\n const extractedB = b ? valueExtractorForRawRow(b) : b\n return compare(extractedA, extractedB)\n }\n\n const index: IndexInterface<string | number> | undefined =\n findIndexForField(\n followRefCollection.indexes,\n followRefResult.path,\n clause.compareOptions\n )\n\n if (index && index.supports(`gt`)) {\n // We found an index that we can use to lazily load ordered data\n const orderByOptimizationInfo = {\n offset: offset ?? 0,\n limit,\n comparator,\n valueExtractorForRawRow,\n index,\n orderBy: orderByClause,\n }\n\n optimizableOrderByCollections[followRefCollection.id] =\n orderByOptimizationInfo\n\n setSizeCallback = (getSize: () => number) => {\n optimizableOrderByCollections[followRefCollection.id] = {\n ...optimizableOrderByCollections[followRefCollection.id]!,\n dataNeeded: () => {\n const size = getSize()\n return Math.max(0, limit - size)\n },\n }\n }\n }\n }\n }\n\n // Use fractional indexing and return the tuple [value, index]\n return pipeline.pipe(\n orderByWithFractionalIndex(valueExtractor, {\n limit,\n offset,\n comparator: compare,\n setSizeCallback,\n })\n // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format\n )\n}\n"],"names":[],"mappings":";;;;;;;AAgCO,SAAS,eACd,UACA,UACA,eACA,cACA,YACA,+BACA,OACA,QAC4D;AAE5D,QAAM,kBAAkB,cAAc,IAAI,CAAC,WAAW;AACpD,UAAM,0BAA0B;AAAA,MAC9B,OAAO;AAAA,MACP;AAAA,MACA;AAAA,IAAA;AAEF,WAAO;AAAA,MACL,oBAAoB,kBAAkB,uBAAuB;AAAA,MAC7D,gBAAgB,OAAO;AAAA,IAAA;AAAA,EAE3B,CAAC;AAGD,QAAM,iBAAiB,CAAC,QAAoD;AAM1E,UAAM,iBAAiB;AAEvB,QAAI,cAAc,SAAS,GAAG;AAE5B,aAAO,gBAAgB;AAAA,QAAI,CAAC,aAC1B,SAAS,mBAAmB,cAAc;AAAA,MAAA;AAAA,IAE9C,WAAW,cAAc,WAAW,GAAG;AAErC,YAAM,WAAW,gBAAgB,CAAC;AAClC,aAAO,SAAS,mBAAmB,cAAc;AAAA,IACnD;AAGA,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,CAAC,GAAY,MAAe;AAE1C,QAAI,cAAc,SAAS,GAAG;AAC5B,YAAM,SAAS;AACf,YAAM,SAAS;AACf,eAAS,IAAI,GAAG,IAAI,cAAc,QAAQ,KAAK;AAC7C,cAAM,SAAS,cAAc,CAAC;AAC9B,cAAM,YAAY,eAAe,OAAO,cAAc;AACtD,cAAM,SAAS,UAAU,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AAC7C,YAAI,WAAW,GAAG;AAChB,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,OAAO,SAAS,OAAO;AAAA,IAChC;AAGA,QAAI,cAAc,WAAW,GAAG;AAC9B,YAAM,SAAS,cAAc,CAAC;AAC9B,YAAM,YAAY,eAAe,OAAO,cAAc;AACtD,aAAO,UAAU,GAAG,CAAC;AAAA,IACvB;AAEA,WAAO,kBAAkB,GAAG,CAAC;AAAA,EAC/B;AAEA,MAAI;AAKJ,MAAI,SAAS,cAAc,WAAW,GAAG;AACvC,UAAM,SAAS,cAAc,CAAC;AAC9B,UAAM,oBAAoB,OAAO;AAEjC,QAAI,kBAAkB,SAAS,OAAO;AACpC,YAAM,kBAAkB;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAGF,YAAM,sBAAsB,gBAAgB;AAC5C,YAAM,YAAY,gBAAgB,KAAK,CAAC;AACxC,UAAI,WAAW;AACb;AAAA,UACE;AAAA,UACA,gBAAgB;AAAA,UAChB;AAAA,UACA,OAAO;AAAA,UACP;AAAA,QAAA;AAAA,MAEJ;AAEA,YAAM,0BAA0B;AAAA,QAC9B,IAAI,QAAQ,gBAAgB,IAAI;AAAA,QAChC;AAAA,MAAA;AAGF,YAAM,aAAa,CACjB,GACA,MACG;AACH,cAAM,aAAa,IAAI,wBAAwB,CAAC,IAAI;AACpD,cAAM,aAAa,IAAI,wBAAwB,CAAC,IAAI;AACpD,eAAO,QAAQ,YAAY,UAAU;AAAA,MACvC;AAEA,YAAM,QACJ;AAAA,QACE,oBAAoB;AAAA,QACpB,gBAAgB;AAAA,QAChB,OAAO;AAAA,MAAA;AAGX,UAAI,SAAS,MAAM,SAAS,IAAI,GAAG;AAEjC,cAAM,0BAA0B;AAAA,UAC9B,QAAQ,UAAU;AAAA,UAClB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,SAAS;AAAA,QAAA;AAGX,sCAA8B,oBAAoB,EAAE,IAClD;AAEF,0BAAkB,CAAC,YAA0B;AAC3C,wCAA8B,oBAAoB,EAAE,IAAI;AAAA,YACtD,GAAG,8BAA8B,oBAAoB,EAAE;AAAA,YACvD,YAAY,MAAM;AAChB,oBAAM,OAAO,QAAA;AACb,qBAAO,KAAK,IAAI,GAAG,QAAQ,IAAI;AAAA,YACjC;AAAA,UAAA;AAAA,QAEJ;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,SAAO,SAAS;AAAA,IACd,2BAA2B,gBAAgB;AAAA,MACzC;AAAA,MACA;AAAA,MACA,YAAY;AAAA,MACZ;AAAA,IAAA,CACD;AAAA;AAAA,EAAA;AAGL;"}
@@ -11,6 +11,7 @@ export declare class CollectionSubscriber<TContext extends Context, TResult exte
11
11
  private syncState;
12
12
  private collectionConfigBuilder;
13
13
  private biggest;
14
+ private collectionAlias;
14
15
  constructor(collectionId: string, collection: Collection, config: Parameters<SyncConfig<TResult>[`sync`]>[0], syncState: FullSyncState, collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>);
15
16
  subscribe(): CollectionSubscription;
16
17
  private subscribeToChanges;