@tanstack/db 0.5.24 → 0.5.25

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 (64) 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/changes.cjs +6 -1
  4. package/dist/cjs/collection/changes.cjs.map +1 -1
  5. package/dist/cjs/collection/lifecycle.cjs +11 -0
  6. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  7. package/dist/cjs/collection/subscription.cjs +18 -5
  8. package/dist/cjs/collection/subscription.cjs.map +1 -1
  9. package/dist/cjs/collection/subscription.d.cts +7 -1
  10. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  11. package/dist/cjs/indexes/base-index.d.cts +10 -6
  12. package/dist/cjs/indexes/btree-index.cjs +64 -24
  13. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  14. package/dist/cjs/indexes/btree-index.d.cts +31 -9
  15. package/dist/cjs/indexes/reverse-index.cjs +6 -0
  16. package/dist/cjs/indexes/reverse-index.cjs.map +1 -1
  17. package/dist/cjs/indexes/reverse-index.d.cts +4 -2
  18. package/dist/cjs/query/live/collection-config-builder.cjs +4 -1
  19. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  20. package/dist/cjs/query/live/collection-subscriber.cjs +111 -30
  21. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  22. package/dist/cjs/query/live/collection-subscriber.d.cts +5 -0
  23. package/dist/cjs/types.d.cts +16 -0
  24. package/dist/cjs/utils/comparison.cjs +16 -0
  25. package/dist/cjs/utils/comparison.cjs.map +1 -1
  26. package/dist/cjs/utils/comparison.d.cts +21 -0
  27. package/dist/esm/collection/change-events.js +1 -1
  28. package/dist/esm/collection/change-events.js.map +1 -1
  29. package/dist/esm/collection/changes.js +6 -1
  30. package/dist/esm/collection/changes.js.map +1 -1
  31. package/dist/esm/collection/lifecycle.js +11 -0
  32. package/dist/esm/collection/lifecycle.js.map +1 -1
  33. package/dist/esm/collection/subscription.d.ts +7 -1
  34. package/dist/esm/collection/subscription.js +18 -5
  35. package/dist/esm/collection/subscription.js.map +1 -1
  36. package/dist/esm/indexes/base-index.d.ts +10 -6
  37. package/dist/esm/indexes/base-index.js.map +1 -1
  38. package/dist/esm/indexes/btree-index.d.ts +31 -9
  39. package/dist/esm/indexes/btree-index.js +65 -25
  40. package/dist/esm/indexes/btree-index.js.map +1 -1
  41. package/dist/esm/indexes/reverse-index.d.ts +4 -2
  42. package/dist/esm/indexes/reverse-index.js +6 -0
  43. package/dist/esm/indexes/reverse-index.js.map +1 -1
  44. package/dist/esm/query/live/collection-config-builder.js +4 -1
  45. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  46. package/dist/esm/query/live/collection-subscriber.d.ts +5 -0
  47. package/dist/esm/query/live/collection-subscriber.js +112 -31
  48. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  49. package/dist/esm/types.d.ts +16 -0
  50. package/dist/esm/utils/comparison.d.ts +21 -0
  51. package/dist/esm/utils/comparison.js +16 -0
  52. package/dist/esm/utils/comparison.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/collection/change-events.ts +1 -1
  55. package/src/collection/changes.ts +6 -1
  56. package/src/collection/lifecycle.ts +14 -0
  57. package/src/collection/subscription.ts +38 -10
  58. package/src/indexes/base-index.ts +19 -6
  59. package/src/indexes/btree-index.ts +101 -30
  60. package/src/indexes/reverse-index.ts +13 -2
  61. package/src/query/live/collection-config-builder.ts +4 -5
  62. package/src/query/live/collection-subscriber.ts +173 -50
  63. package/src/types.ts +16 -0
  64. package/src/utils/comparison.ts +34 -0
@@ -8,8 +8,11 @@ export declare class CollectionSubscriber<TContext extends Context, TResult exte
8
8
  private collection;
9
9
  private collectionConfigBuilder;
10
10
  private biggest;
11
+ private lastLoadRequestKey;
11
12
  private subscriptionLoadingPromises;
12
13
  private sentToD2Keys;
14
+ private orderedLoadSubsetResult?;
15
+ private pendingOrderedLoadPromise;
13
16
  constructor(alias: string, collectionId: string, collection: Collection, collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>);
14
17
  subscribe(): CollectionSubscription;
15
18
  private subscribeToChanges;
@@ -22,4 +25,6 @@ export declare class CollectionSubscriber<TContext extends Context, TResult exte
22
25
  private getWhereClauseForAlias;
23
26
  private getOrderByInfo;
24
27
  private trackSentValues;
28
+ private ensureLoadingPromise;
29
+ private getLoadRequestKey;
25
30
  }
@@ -628,6 +628,22 @@ export interface SubscribeChangesOptions<T extends object = Record<string, unkno
628
628
  * @internal
629
629
  */
630
630
  onStatusChange?: (event: SubscriptionStatusChangeEvent) => void;
631
+ /**
632
+ * Optional orderBy to include in loadSubset for query-specific cache keys.
633
+ * @internal
634
+ */
635
+ orderBy?: OrderBy;
636
+ /**
637
+ * Optional limit to include in loadSubset for query-specific cache keys.
638
+ * @internal
639
+ */
640
+ limit?: number;
641
+ /**
642
+ * Callback that receives the loadSubset result (Promise or true) from requestSnapshot.
643
+ * Allows the caller to directly track the loading promise for isReady status.
644
+ * @internal
645
+ */
646
+ onLoadSubsetResult?: (result: Promise<void> | true) => void;
631
647
  }
632
648
  export interface SubscribeChangesSnapshotOptions<T extends object = Record<string, unknown>> extends Omit<SubscribeChangesOptions<T>, `includeInitialState`> {
633
649
  orderBy?: OrderBy;
@@ -79,6 +79,7 @@ function areUint8ArraysEqual(a, b) {
79
79
  return true;
80
80
  }
81
81
  const UINT8ARRAY_NORMALIZE_THRESHOLD = 128;
82
+ const UNDEFINED_SENTINEL = `__TS_DB_BTREE_UNDEFINED_VALUE__`;
82
83
  function normalizeValue(value) {
83
84
  if (value instanceof Date) {
84
85
  return value.getTime();
@@ -91,6 +92,18 @@ function normalizeValue(value) {
91
92
  }
92
93
  return value;
93
94
  }
95
+ function normalizeForBTree(value) {
96
+ if (value === void 0) {
97
+ return UNDEFINED_SENTINEL;
98
+ }
99
+ return normalizeValue(value);
100
+ }
101
+ function denormalizeUndefined(value) {
102
+ if (value === UNDEFINED_SENTINEL) {
103
+ return void 0;
104
+ }
105
+ return value;
106
+ }
94
107
  function areValuesEqual(a, b) {
95
108
  if (a === b) {
96
109
  return true;
@@ -102,10 +115,13 @@ function areValuesEqual(a, b) {
102
115
  }
103
116
  return false;
104
117
  }
118
+ exports.UNDEFINED_SENTINEL = UNDEFINED_SENTINEL;
105
119
  exports.areValuesEqual = areValuesEqual;
106
120
  exports.ascComparator = ascComparator;
107
121
  exports.defaultComparator = defaultComparator;
122
+ exports.denormalizeUndefined = denormalizeUndefined;
108
123
  exports.descComparator = descComparator;
109
124
  exports.makeComparator = makeComparator;
125
+ exports.normalizeForBTree = normalizeForBTree;
110
126
  exports.normalizeValue = normalizeValue;
111
127
  //# sourceMappingURL=comparison.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"comparison.cjs","sources":["../../../src/utils/comparison.ts"],"sourcesContent":["import type { CompareOptions } from '../query/builder/types'\n\n// WeakMap to store stable IDs for objects\nconst objectIds = new WeakMap<object, number>()\nlet nextObjectId = 1\n\n/**\n * Get or create a stable ID for an object\n */\nfunction getObjectId(obj: object): number {\n if (objectIds.has(obj)) {\n return objectIds.get(obj)!\n }\n const id = nextObjectId++\n objectIds.set(obj, id)\n return id\n}\n\n/**\n * Universal comparison function for all data types\n * Handles null/undefined, strings, arrays, dates, objects, and primitives\n * Always sorts null/undefined values first\n */\nexport const ascComparator = (a: any, b: any, opts: CompareOptions): number => {\n const { nulls } = opts\n\n // Handle null/undefined\n if (a == null && b == null) return 0\n if (a == null) return nulls === `first` ? -1 : 1\n if (b == null) return nulls === `first` ? 1 : -1\n\n // if a and b are both strings, compare them based on locale\n if (typeof a === `string` && typeof b === `string`) {\n if (opts.stringSort === `locale`) {\n return a.localeCompare(b, opts.locale, opts.localeOptions)\n }\n // For lexical sort we rely on direct comparison for primitive values\n }\n\n // if a and b are both arrays, compare them element by element\n if (Array.isArray(a) && Array.isArray(b)) {\n for (let i = 0; i < Math.min(a.length, b.length); i++) {\n const result = ascComparator(a[i], b[i], opts)\n if (result !== 0) {\n return result\n }\n }\n // All elements are equal up to the minimum length\n return a.length - b.length\n }\n\n // If both are dates, compare them\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime()\n }\n\n // If at least one of the values is an object, use stable IDs for comparison\n const aIsObject = typeof a === `object`\n const bIsObject = typeof b === `object`\n\n if (aIsObject || bIsObject) {\n // If both are objects, compare their stable IDs\n if (aIsObject && bIsObject) {\n const aId = getObjectId(a)\n const bId = getObjectId(b)\n return aId - bId\n }\n\n // If only one is an object, objects come after primitives\n if (aIsObject) return 1\n if (bIsObject) return -1\n }\n\n // For primitive values, use direct comparison\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n\n/**\n * Descending comparator function for ordering values\n * Handles null/undefined as largest values (opposite of ascending)\n */\nexport const descComparator = (\n a: unknown,\n b: unknown,\n opts: CompareOptions,\n): number => {\n return ascComparator(b, a, {\n ...opts,\n nulls: opts.nulls === `first` ? `last` : `first`,\n })\n}\n\nexport function makeComparator(\n opts: CompareOptions,\n): (a: any, b: any) => number {\n return (a, b) => {\n if (opts.direction === `asc`) {\n return ascComparator(a, b, opts)\n } else {\n return descComparator(a, b, opts)\n }\n }\n}\n\n/** Default comparator orders values in ascending order with nulls first and locale string comparison. */\nexport const defaultComparator = makeComparator({\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n})\n\n/**\n * Compare two Uint8Arrays for content equality\n */\nfunction areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.byteLength !== b.byteLength) {\n return false\n }\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) {\n return false\n }\n }\n return true\n}\n\n/**\n * Threshold for normalizing Uint8Arrays to string representations.\n * Arrays larger than this will use reference equality to avoid memory overhead.\n * 128 bytes is enough for common ID formats (ULIDs are 16 bytes, UUIDs are 16 bytes)\n * while avoiding excessive string allocation for large binary data.\n */\nconst UINT8ARRAY_NORMALIZE_THRESHOLD = 128\n\n/**\n * Normalize a value for comparison and Map key usage\n * Converts values that can't be directly compared or used as Map keys\n * into comparable primitive representations\n */\nexport function normalizeValue(value: any): any {\n if (value instanceof Date) {\n return value.getTime()\n }\n\n // Normalize Uint8Arrays/Buffers to a string representation for Map key usage\n // This enables content-based equality for binary data like ULIDs\n const isUint8Array =\n (typeof Buffer !== `undefined` && value instanceof Buffer) ||\n value instanceof Uint8Array\n\n if (isUint8Array) {\n // Only normalize small arrays to avoid memory overhead for large binary data\n if (value.byteLength <= UINT8ARRAY_NORMALIZE_THRESHOLD) {\n // Convert to a string representation that can be used as a Map key\n // Use a special prefix to avoid collisions with user strings\n return `__u8__${Array.from(value).join(`,`)}`\n }\n // For large arrays, fall back to reference equality\n // Users working with large binary data should use a derived key if needed\n }\n\n return value\n}\n\n/**\n * Compare two values for equality, with special handling for Uint8Arrays and Buffers\n */\nexport function areValuesEqual(a: any, b: any): boolean {\n // Fast path for reference equality\n if (a === b) {\n return true\n }\n\n // Check for Uint8Array/Buffer comparison\n const aIsUint8Array =\n (typeof Buffer !== `undefined` && a instanceof Buffer) ||\n a instanceof Uint8Array\n const bIsUint8Array =\n (typeof Buffer !== `undefined` && b instanceof Buffer) ||\n b instanceof Uint8Array\n\n // If both are Uint8Arrays, compare by content\n if (aIsUint8Array && bIsUint8Array) {\n return areUint8ArraysEqual(a, b)\n }\n\n // Different types or not Uint8Arrays\n return false\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"comparison.cjs","sources":["../../../src/utils/comparison.ts"],"sourcesContent":["import type { CompareOptions } from '../query/builder/types'\n\n// WeakMap to store stable IDs for objects\nconst objectIds = new WeakMap<object, number>()\nlet nextObjectId = 1\n\n/**\n * Get or create a stable ID for an object\n */\nfunction getObjectId(obj: object): number {\n if (objectIds.has(obj)) {\n return objectIds.get(obj)!\n }\n const id = nextObjectId++\n objectIds.set(obj, id)\n return id\n}\n\n/**\n * Universal comparison function for all data types\n * Handles null/undefined, strings, arrays, dates, objects, and primitives\n * Always sorts null/undefined values first\n */\nexport const ascComparator = (a: any, b: any, opts: CompareOptions): number => {\n const { nulls } = opts\n\n // Handle null/undefined\n if (a == null && b == null) return 0\n if (a == null) return nulls === `first` ? -1 : 1\n if (b == null) return nulls === `first` ? 1 : -1\n\n // if a and b are both strings, compare them based on locale\n if (typeof a === `string` && typeof b === `string`) {\n if (opts.stringSort === `locale`) {\n return a.localeCompare(b, opts.locale, opts.localeOptions)\n }\n // For lexical sort we rely on direct comparison for primitive values\n }\n\n // if a and b are both arrays, compare them element by element\n if (Array.isArray(a) && Array.isArray(b)) {\n for (let i = 0; i < Math.min(a.length, b.length); i++) {\n const result = ascComparator(a[i], b[i], opts)\n if (result !== 0) {\n return result\n }\n }\n // All elements are equal up to the minimum length\n return a.length - b.length\n }\n\n // If both are dates, compare them\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime()\n }\n\n // If at least one of the values is an object, use stable IDs for comparison\n const aIsObject = typeof a === `object`\n const bIsObject = typeof b === `object`\n\n if (aIsObject || bIsObject) {\n // If both are objects, compare their stable IDs\n if (aIsObject && bIsObject) {\n const aId = getObjectId(a)\n const bId = getObjectId(b)\n return aId - bId\n }\n\n // If only one is an object, objects come after primitives\n if (aIsObject) return 1\n if (bIsObject) return -1\n }\n\n // For primitive values, use direct comparison\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n\n/**\n * Descending comparator function for ordering values\n * Handles null/undefined as largest values (opposite of ascending)\n */\nexport const descComparator = (\n a: unknown,\n b: unknown,\n opts: CompareOptions,\n): number => {\n return ascComparator(b, a, {\n ...opts,\n nulls: opts.nulls === `first` ? `last` : `first`,\n })\n}\n\nexport function makeComparator(\n opts: CompareOptions,\n): (a: any, b: any) => number {\n return (a, b) => {\n if (opts.direction === `asc`) {\n return ascComparator(a, b, opts)\n } else {\n return descComparator(a, b, opts)\n }\n }\n}\n\n/** Default comparator orders values in ascending order with nulls first and locale string comparison. */\nexport const defaultComparator = makeComparator({\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n})\n\n/**\n * Compare two Uint8Arrays for content equality\n */\nfunction areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.byteLength !== b.byteLength) {\n return false\n }\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) {\n return false\n }\n }\n return true\n}\n\n/**\n * Threshold for normalizing Uint8Arrays to string representations.\n * Arrays larger than this will use reference equality to avoid memory overhead.\n * 128 bytes is enough for common ID formats (ULIDs are 16 bytes, UUIDs are 16 bytes)\n * while avoiding excessive string allocation for large binary data.\n */\nconst UINT8ARRAY_NORMALIZE_THRESHOLD = 128\n\n/**\n * Sentinel value representing undefined in normalized form.\n * This allows distinguishing between \"start from beginning\" (undefined parameter)\n * and \"start from the key undefined\" (actual undefined value in the tree).\n */\nexport const UNDEFINED_SENTINEL = `__TS_DB_BTREE_UNDEFINED_VALUE__`\n\n/**\n * Normalize a value for comparison and Map key usage\n * Converts values that can't be directly compared or used as Map keys\n * into comparable primitive representations\n *\n * Note: This does NOT convert undefined to a sentinel. Use normalizeForBTree\n * for BTree index operations that need to distinguish undefined values.\n */\nexport function normalizeValue(value: any): any {\n if (value instanceof Date) {\n return value.getTime()\n }\n\n // Normalize Uint8Arrays/Buffers to a string representation for Map key usage\n // This enables content-based equality for binary data like ULIDs\n const isUint8Array =\n (typeof Buffer !== `undefined` && value instanceof Buffer) ||\n value instanceof Uint8Array\n\n if (isUint8Array) {\n // Only normalize small arrays to avoid memory overhead for large binary data\n if (value.byteLength <= UINT8ARRAY_NORMALIZE_THRESHOLD) {\n // Convert to a string representation that can be used as a Map key\n // Use a special prefix to avoid collisions with user strings\n return `__u8__${Array.from(value).join(`,`)}`\n }\n // For large arrays, fall back to reference equality\n // Users working with large binary data should use a derived key if needed\n }\n\n return value\n}\n\n/**\n * Normalize a value for BTree index usage.\n * Extends normalizeValue to also convert undefined to a sentinel value.\n * This is needed because the BTree does not properly support `undefined` as a key\n * (it interprets undefined as \"start from beginning\" in nextHigherPair/nextLowerPair).\n */\nexport function normalizeForBTree(value: any): any {\n if (value === undefined) {\n return UNDEFINED_SENTINEL\n }\n return normalizeValue(value)\n}\n\n/**\n * Converts the `UNDEFINED_SENTINEL` back to `undefined`.\n * Needed such that the sentinel is converted back to `undefined` before comparison.\n */\nexport function denormalizeUndefined(value: any): any {\n if (value === UNDEFINED_SENTINEL) {\n return undefined\n }\n return value\n}\n\n/**\n * Compare two values for equality, with special handling for Uint8Arrays and Buffers\n */\nexport function areValuesEqual(a: any, b: any): boolean {\n // Fast path for reference equality\n if (a === b) {\n return true\n }\n\n // Check for Uint8Array/Buffer comparison\n const aIsUint8Array =\n (typeof Buffer !== `undefined` && a instanceof Buffer) ||\n a instanceof Uint8Array\n const bIsUint8Array =\n (typeof Buffer !== `undefined` && b instanceof Buffer) ||\n b instanceof Uint8Array\n\n // If both are Uint8Arrays, compare by content\n if (aIsUint8Array && bIsUint8Array) {\n return areUint8ArraysEqual(a, b)\n }\n\n // Different types or not Uint8Arrays\n return false\n}\n"],"names":[],"mappings":"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"}
@@ -13,12 +13,33 @@ export declare const descComparator: (a: unknown, b: unknown, opts: CompareOptio
13
13
  export declare function makeComparator(opts: CompareOptions): (a: any, b: any) => number;
14
14
  /** Default comparator orders values in ascending order with nulls first and locale string comparison. */
15
15
  export declare const defaultComparator: (a: any, b: any) => number;
16
+ /**
17
+ * Sentinel value representing undefined in normalized form.
18
+ * This allows distinguishing between "start from beginning" (undefined parameter)
19
+ * and "start from the key undefined" (actual undefined value in the tree).
20
+ */
21
+ export declare const UNDEFINED_SENTINEL = "__TS_DB_BTREE_UNDEFINED_VALUE__";
16
22
  /**
17
23
  * Normalize a value for comparison and Map key usage
18
24
  * Converts values that can't be directly compared or used as Map keys
19
25
  * into comparable primitive representations
26
+ *
27
+ * Note: This does NOT convert undefined to a sentinel. Use normalizeForBTree
28
+ * for BTree index operations that need to distinguish undefined values.
20
29
  */
21
30
  export declare function normalizeValue(value: any): any;
31
+ /**
32
+ * Normalize a value for BTree index usage.
33
+ * Extends normalizeValue to also convert undefined to a sentinel value.
34
+ * This is needed because the BTree does not properly support `undefined` as a key
35
+ * (it interprets undefined as "start from beginning" in nextHigherPair/nextLowerPair).
36
+ */
37
+ export declare function normalizeForBTree(value: any): any;
38
+ /**
39
+ * Converts the `UNDEFINED_SENTINEL` back to `undefined`.
40
+ * Needed such that the sentinel is converted back to `undefined` before comparison.
41
+ */
42
+ export declare function denormalizeUndefined(value: any): any;
22
43
  /**
23
44
  * Compare two values for equality, with special handling for Uint8Arrays and Buffers
24
45
  */
@@ -158,7 +158,7 @@ function getOrderedKeys(collection, orderBy, limit, whereFilter, optimizedOnly)
158
158
  }
159
159
  return whereFilter?.(value) ?? true;
160
160
  };
161
- return index.take(limit ?? index.keyCount, void 0, filterFn);
161
+ return index.takeFromStart(limit ?? index.keyCount, filterFn);
162
162
  }
163
163
  }
164
164
  }
@@ -1 +1 @@
1
- {"version":3,"file":"change-events.js","sources":["../../../src/collection/change-events.ts"],"sourcesContent":["import {\n createSingleRowRefProxy,\n toExpression,\n} from '../query/builder/ref-proxy'\nimport {\n compileSingleRowExpression,\n toBooleanPredicate,\n} from '../query/compiler/evaluators.js'\nimport {\n findIndexForField,\n optimizeExpressionWithIndexes,\n} from '../utils/index-optimization.js'\nimport { ensureIndexForField } from '../indexes/auto-index.js'\nimport { makeComparator } from '../utils/comparison.js'\nimport { buildCompareOptions } from '../query/compiler/order-by'\nimport type {\n ChangeMessage,\n CollectionLike,\n CurrentStateAsChangesOptions,\n SubscribeChangesOptions,\n} from '../types'\nimport type { CollectionImpl } from './index.js'\nimport type { SingleRowRefProxy } from '../query/builder/ref-proxy'\nimport type { BasicExpression, OrderBy } from '../query/ir.js'\n\n/**\n * Returns the current state of the collection as an array of changes\n * @param collection - The collection to get changes from\n * @param options - Options including optional where filter, orderBy, and limit\n * @returns An array of changes\n * @example\n * // Get all items as changes\n * const allChanges = currentStateAsChanges(collection)\n *\n * // Get only items matching a condition\n * const activeChanges = currentStateAsChanges(collection, {\n * where: (row) => row.status === 'active'\n * })\n *\n * // Get only items using a pre-compiled expression\n * const activeChanges = currentStateAsChanges(collection, {\n * where: eq(row.status, 'active')\n * })\n *\n * // Get items ordered by name with limit\n * const topUsers = currentStateAsChanges(collection, {\n * orderBy: [{ expression: row.name, compareOptions: { direction: 'asc' } }],\n * limit: 10\n * })\n *\n * // Get active users ordered by score (highest score first)\n * const topActiveUsers = currentStateAsChanges(collection, {\n * where: eq(row.status, 'active'),\n * orderBy: [{ expression: row.score, compareOptions: { direction: 'desc' } }],\n * })\n */\nexport function currentStateAsChanges<\n T extends object,\n TKey extends string | number,\n>(\n collection: CollectionLike<T, TKey>,\n options: CurrentStateAsChangesOptions = {},\n): Array<ChangeMessage<T>> | void {\n // Helper function to collect filtered results\n const collectFilteredResults = (\n filterFn?: (value: T) => boolean,\n ): Array<ChangeMessage<T>> => {\n const result: Array<ChangeMessage<T>> = []\n for (const [key, value] of collection.entries()) {\n // If no filter function is provided, include all items\n if (filterFn?.(value) ?? true) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n }\n\n // Validate that limit without orderBy doesn't happen\n if (options.limit !== undefined && !options.orderBy) {\n throw new Error(`limit cannot be used without orderBy`)\n }\n\n // First check if orderBy is present (optionally with limit)\n if (options.orderBy) {\n // Create where filter function if present\n const whereFilter = options.where\n ? createFilterFunctionFromExpression(options.where)\n : undefined\n\n // Get ordered keys using index optimization when possible\n const orderedKeys = getOrderedKeys(\n collection,\n options.orderBy,\n options.limit,\n whereFilter,\n options.optimizedOnly,\n )\n\n if (orderedKeys === undefined) {\n // `getOrderedKeys` returned undefined because we asked for `optimizedOnly` and there was no index to use\n return\n }\n\n // Convert keys to change messages\n const result: Array<ChangeMessage<T>> = []\n for (const key of orderedKeys) {\n const value = collection.get(key)\n if (value !== undefined) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n }\n\n // If no orderBy OR orderBy optimization failed, use where clause optimization\n if (!options.where) {\n // No filtering, return all items\n return collectFilteredResults()\n }\n\n // There's a where clause, let's see if we can use an index\n try {\n const expression: BasicExpression<boolean> = options.where\n\n // Try to optimize the query using indexes\n const optimizationResult = optimizeExpressionWithIndexes(\n expression,\n collection,\n )\n\n if (optimizationResult.canOptimize) {\n // Use index optimization\n const result: Array<ChangeMessage<T>> = []\n for (const key of optimizationResult.matchingKeys) {\n const value = collection.get(key)\n if (value !== undefined) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n } else {\n if (options.optimizedOnly) {\n return\n }\n\n const filterFn = createFilterFunctionFromExpression(expression)\n return collectFilteredResults(filterFn)\n }\n } catch (error) {\n // If anything goes wrong with the where clause, fall back to full scan\n console.warn(\n `${collection.id ? `[${collection.id}] ` : ``}Error processing where clause, falling back to full scan:`,\n error,\n )\n\n const filterFn = createFilterFunctionFromExpression(options.where)\n\n if (options.optimizedOnly) {\n return\n }\n\n return collectFilteredResults(filterFn)\n }\n}\n\n/**\n * Creates a filter function from a where callback\n * @param whereCallback - The callback function that defines the filter condition\n * @returns A function that takes an item and returns true if it matches the filter\n */\nexport function createFilterFunction<T extends object>(\n whereCallback: (row: SingleRowRefProxy<T>) => any,\n): (item: T) => boolean {\n return (item: T): boolean => {\n try {\n // First try the RefProxy approach for query builder functions\n const singleRowRefProxy = createSingleRowRefProxy<T>()\n const whereExpression = whereCallback(singleRowRefProxy)\n const expression = toExpression(whereExpression)\n const evaluator = compileSingleRowExpression(expression)\n const result = evaluator(item as Record<string, unknown>)\n // WHERE clauses should always evaluate to boolean predicates (Kevin's feedback)\n return toBooleanPredicate(result)\n } catch {\n // If RefProxy approach fails (e.g., arithmetic operations), fall back to direct evaluation\n try {\n // Create a simple proxy that returns actual values for arithmetic operations\n const simpleProxy = new Proxy(item as any, {\n get(target, prop) {\n return target[prop]\n },\n }) as SingleRowRefProxy<T>\n\n const result = whereCallback(simpleProxy)\n return toBooleanPredicate(result)\n } catch {\n // If both approaches fail, exclude the item\n return false\n }\n }\n }\n}\n\n/**\n * Creates a filter function from a pre-compiled expression\n * @param expression - The pre-compiled expression to evaluate\n * @returns A function that takes an item and returns true if it matches the filter\n */\nexport function createFilterFunctionFromExpression<T extends object>(\n expression: BasicExpression<boolean>,\n): (item: T) => boolean {\n // Compile expression once when filter function is created, not on every invocation\n const evaluator = compileSingleRowExpression(expression)\n\n return (item: T): boolean => {\n try {\n const result = evaluator(item as Record<string, unknown>)\n return toBooleanPredicate(result)\n } catch {\n // If evaluation fails, exclude the item\n return false\n }\n }\n}\n\n/**\n * Creates a filtered callback that only calls the original callback with changes that match the where clause\n * @param originalCallback - The original callback to filter\n * @param options - The subscription options containing the where clause\n * @returns A filtered callback function\n */\nexport function createFilteredCallback<T extends object>(\n originalCallback: (changes: Array<ChangeMessage<T>>) => void,\n options: SubscribeChangesOptions,\n): (changes: Array<ChangeMessage<T>>) => void {\n const filterFn = createFilterFunctionFromExpression(options.whereExpression!)\n\n return (changes: Array<ChangeMessage<T>>) => {\n const filteredChanges: Array<ChangeMessage<T>> = []\n\n for (const change of changes) {\n if (change.type === `insert`) {\n // For inserts, check if the new value matches the filter\n if (filterFn(change.value)) {\n filteredChanges.push(change)\n }\n } else if (change.type === `update`) {\n // For updates, we need to check both old and new values\n const newValueMatches = filterFn(change.value)\n const oldValueMatches = change.previousValue\n ? filterFn(change.previousValue)\n : false\n\n if (newValueMatches && oldValueMatches) {\n // Both old and new match: emit update\n filteredChanges.push(change)\n } else if (newValueMatches && !oldValueMatches) {\n // New matches but old didn't: emit insert\n filteredChanges.push({\n ...change,\n type: `insert`,\n })\n } else if (!newValueMatches && oldValueMatches) {\n // Old matched but new doesn't: emit delete\n filteredChanges.push({\n ...change,\n type: `delete`,\n value: change.previousValue!, // Use the previous value for the delete\n })\n }\n // If neither matches, don't emit anything\n } else {\n // For deletes, include if the previous value would have matched\n // (so subscribers know something they were tracking was deleted)\n if (filterFn(change.value)) {\n filteredChanges.push(change)\n }\n }\n }\n\n // Always call the original callback if we have filtered changes OR\n // if the original changes array was empty (which indicates a ready signal)\n if (filteredChanges.length > 0 || changes.length === 0) {\n originalCallback(filteredChanges)\n }\n }\n}\n\n/**\n * Gets ordered keys from a collection using index optimization when possible\n * @param collection - The collection to get keys from\n * @param orderBy - The order by clause\n * @param limit - Optional limit on number of keys to return\n * @param whereFilter - Optional filter function to apply while traversing\n * @returns Array of keys in sorted order\n */\nfunction getOrderedKeys<T extends object, TKey extends string | number>(\n collection: CollectionLike<T, TKey>,\n orderBy: OrderBy,\n limit?: number,\n whereFilter?: (item: T) => boolean,\n optimizedOnly?: boolean,\n): Array<TKey> | undefined {\n // For single-column orderBy on a ref expression, try index optimization\n if (orderBy.length === 1) {\n const clause = orderBy[0]!\n const orderByExpression = clause.expression\n\n if (orderByExpression.type === `ref`) {\n const propRef = orderByExpression\n const fieldPath = propRef.path\n const compareOpts = buildCompareOptions(clause, collection)\n\n // Ensure index exists for this field\n ensureIndexForField(\n fieldPath[0]!,\n fieldPath,\n collection as CollectionImpl<T, TKey>,\n compareOpts,\n )\n\n // Find the index\n const index = findIndexForField(collection, fieldPath, compareOpts)\n\n if (index && index.supports(`gt`)) {\n // Use index optimization\n const filterFn = (key: TKey): boolean => {\n const value = collection.get(key)\n if (value === undefined) {\n return false\n }\n return whereFilter?.(value) ?? true\n }\n\n // Take the keys that match the filter and limit\n // if no limit is provided `index.keyCount` is used,\n // i.e. we will take all keys that match the filter\n return index.take(limit ?? index.keyCount, undefined, filterFn)\n }\n }\n }\n\n if (optimizedOnly) {\n return\n }\n\n // Fallback: collect all items and sort in memory\n const allItems: Array<{ key: TKey; value: T }> = []\n for (const [key, value] of collection.entries()) {\n if (whereFilter?.(value) ?? true) {\n allItems.push({ key, value })\n }\n }\n\n // Sort using makeComparator\n const compare = (a: { key: TKey; value: T }, b: { key: TKey; value: T }) => {\n for (const clause of orderBy) {\n const compareFn = makeComparator(clause.compareOptions)\n\n // Extract values for comparison\n const aValue = extractValueFromItem(a.value, clause.expression)\n const bValue = extractValueFromItem(b.value, clause.expression)\n\n const result = compareFn(aValue, bValue)\n if (result !== 0) {\n return result\n }\n }\n return 0\n }\n\n allItems.sort(compare)\n const sortedKeys = allItems.map((item) => item.key)\n\n // Apply limit if provided\n if (limit !== undefined) {\n return sortedKeys.slice(0, limit)\n }\n\n // if no limit is provided, we will return all keys\n return sortedKeys\n}\n\n/**\n * Helper function to extract a value from an item based on an expression\n */\nfunction extractValueFromItem(item: any, expression: BasicExpression): any {\n if (expression.type === `ref`) {\n const propRef = expression\n let value = item\n for (const pathPart of propRef.path) {\n value = value?.[pathPart]\n }\n return value\n } else if (expression.type === `val`) {\n return expression.value\n } else {\n // It must be a function\n const evaluator = compileSingleRowExpression(expression)\n return evaluator(item as Record<string, unknown>)\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"change-events.js","sources":["../../../src/collection/change-events.ts"],"sourcesContent":["import {\n createSingleRowRefProxy,\n toExpression,\n} from '../query/builder/ref-proxy'\nimport {\n compileSingleRowExpression,\n toBooleanPredicate,\n} from '../query/compiler/evaluators.js'\nimport {\n findIndexForField,\n optimizeExpressionWithIndexes,\n} from '../utils/index-optimization.js'\nimport { ensureIndexForField } from '../indexes/auto-index.js'\nimport { makeComparator } from '../utils/comparison.js'\nimport { buildCompareOptions } from '../query/compiler/order-by'\nimport type {\n ChangeMessage,\n CollectionLike,\n CurrentStateAsChangesOptions,\n SubscribeChangesOptions,\n} from '../types'\nimport type { CollectionImpl } from './index.js'\nimport type { SingleRowRefProxy } from '../query/builder/ref-proxy'\nimport type { BasicExpression, OrderBy } from '../query/ir.js'\n\n/**\n * Returns the current state of the collection as an array of changes\n * @param collection - The collection to get changes from\n * @param options - Options including optional where filter, orderBy, and limit\n * @returns An array of changes\n * @example\n * // Get all items as changes\n * const allChanges = currentStateAsChanges(collection)\n *\n * // Get only items matching a condition\n * const activeChanges = currentStateAsChanges(collection, {\n * where: (row) => row.status === 'active'\n * })\n *\n * // Get only items using a pre-compiled expression\n * const activeChanges = currentStateAsChanges(collection, {\n * where: eq(row.status, 'active')\n * })\n *\n * // Get items ordered by name with limit\n * const topUsers = currentStateAsChanges(collection, {\n * orderBy: [{ expression: row.name, compareOptions: { direction: 'asc' } }],\n * limit: 10\n * })\n *\n * // Get active users ordered by score (highest score first)\n * const topActiveUsers = currentStateAsChanges(collection, {\n * where: eq(row.status, 'active'),\n * orderBy: [{ expression: row.score, compareOptions: { direction: 'desc' } }],\n * })\n */\nexport function currentStateAsChanges<\n T extends object,\n TKey extends string | number,\n>(\n collection: CollectionLike<T, TKey>,\n options: CurrentStateAsChangesOptions = {},\n): Array<ChangeMessage<T>> | void {\n // Helper function to collect filtered results\n const collectFilteredResults = (\n filterFn?: (value: T) => boolean,\n ): Array<ChangeMessage<T>> => {\n const result: Array<ChangeMessage<T>> = []\n for (const [key, value] of collection.entries()) {\n // If no filter function is provided, include all items\n if (filterFn?.(value) ?? true) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n }\n\n // Validate that limit without orderBy doesn't happen\n if (options.limit !== undefined && !options.orderBy) {\n throw new Error(`limit cannot be used without orderBy`)\n }\n\n // First check if orderBy is present (optionally with limit)\n if (options.orderBy) {\n // Create where filter function if present\n const whereFilter = options.where\n ? createFilterFunctionFromExpression(options.where)\n : undefined\n\n // Get ordered keys using index optimization when possible\n const orderedKeys = getOrderedKeys(\n collection,\n options.orderBy,\n options.limit,\n whereFilter,\n options.optimizedOnly,\n )\n\n if (orderedKeys === undefined) {\n // `getOrderedKeys` returned undefined because we asked for `optimizedOnly` and there was no index to use\n return\n }\n\n // Convert keys to change messages\n const result: Array<ChangeMessage<T>> = []\n for (const key of orderedKeys) {\n const value = collection.get(key)\n if (value !== undefined) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n }\n\n // If no orderBy OR orderBy optimization failed, use where clause optimization\n if (!options.where) {\n // No filtering, return all items\n return collectFilteredResults()\n }\n\n // There's a where clause, let's see if we can use an index\n try {\n const expression: BasicExpression<boolean> = options.where\n\n // Try to optimize the query using indexes\n const optimizationResult = optimizeExpressionWithIndexes(\n expression,\n collection,\n )\n\n if (optimizationResult.canOptimize) {\n // Use index optimization\n const result: Array<ChangeMessage<T>> = []\n for (const key of optimizationResult.matchingKeys) {\n const value = collection.get(key)\n if (value !== undefined) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n } else {\n if (options.optimizedOnly) {\n return\n }\n\n const filterFn = createFilterFunctionFromExpression(expression)\n return collectFilteredResults(filterFn)\n }\n } catch (error) {\n // If anything goes wrong with the where clause, fall back to full scan\n console.warn(\n `${collection.id ? `[${collection.id}] ` : ``}Error processing where clause, falling back to full scan:`,\n error,\n )\n\n const filterFn = createFilterFunctionFromExpression(options.where)\n\n if (options.optimizedOnly) {\n return\n }\n\n return collectFilteredResults(filterFn)\n }\n}\n\n/**\n * Creates a filter function from a where callback\n * @param whereCallback - The callback function that defines the filter condition\n * @returns A function that takes an item and returns true if it matches the filter\n */\nexport function createFilterFunction<T extends object>(\n whereCallback: (row: SingleRowRefProxy<T>) => any,\n): (item: T) => boolean {\n return (item: T): boolean => {\n try {\n // First try the RefProxy approach for query builder functions\n const singleRowRefProxy = createSingleRowRefProxy<T>()\n const whereExpression = whereCallback(singleRowRefProxy)\n const expression = toExpression(whereExpression)\n const evaluator = compileSingleRowExpression(expression)\n const result = evaluator(item as Record<string, unknown>)\n // WHERE clauses should always evaluate to boolean predicates (Kevin's feedback)\n return toBooleanPredicate(result)\n } catch {\n // If RefProxy approach fails (e.g., arithmetic operations), fall back to direct evaluation\n try {\n // Create a simple proxy that returns actual values for arithmetic operations\n const simpleProxy = new Proxy(item as any, {\n get(target, prop) {\n return target[prop]\n },\n }) as SingleRowRefProxy<T>\n\n const result = whereCallback(simpleProxy)\n return toBooleanPredicate(result)\n } catch {\n // If both approaches fail, exclude the item\n return false\n }\n }\n }\n}\n\n/**\n * Creates a filter function from a pre-compiled expression\n * @param expression - The pre-compiled expression to evaluate\n * @returns A function that takes an item and returns true if it matches the filter\n */\nexport function createFilterFunctionFromExpression<T extends object>(\n expression: BasicExpression<boolean>,\n): (item: T) => boolean {\n // Compile expression once when filter function is created, not on every invocation\n const evaluator = compileSingleRowExpression(expression)\n\n return (item: T): boolean => {\n try {\n const result = evaluator(item as Record<string, unknown>)\n return toBooleanPredicate(result)\n } catch {\n // If evaluation fails, exclude the item\n return false\n }\n }\n}\n\n/**\n * Creates a filtered callback that only calls the original callback with changes that match the where clause\n * @param originalCallback - The original callback to filter\n * @param options - The subscription options containing the where clause\n * @returns A filtered callback function\n */\nexport function createFilteredCallback<T extends object>(\n originalCallback: (changes: Array<ChangeMessage<T>>) => void,\n options: SubscribeChangesOptions,\n): (changes: Array<ChangeMessage<T>>) => void {\n const filterFn = createFilterFunctionFromExpression(options.whereExpression!)\n\n return (changes: Array<ChangeMessage<T>>) => {\n const filteredChanges: Array<ChangeMessage<T>> = []\n\n for (const change of changes) {\n if (change.type === `insert`) {\n // For inserts, check if the new value matches the filter\n if (filterFn(change.value)) {\n filteredChanges.push(change)\n }\n } else if (change.type === `update`) {\n // For updates, we need to check both old and new values\n const newValueMatches = filterFn(change.value)\n const oldValueMatches = change.previousValue\n ? filterFn(change.previousValue)\n : false\n\n if (newValueMatches && oldValueMatches) {\n // Both old and new match: emit update\n filteredChanges.push(change)\n } else if (newValueMatches && !oldValueMatches) {\n // New matches but old didn't: emit insert\n filteredChanges.push({\n ...change,\n type: `insert`,\n })\n } else if (!newValueMatches && oldValueMatches) {\n // Old matched but new doesn't: emit delete\n filteredChanges.push({\n ...change,\n type: `delete`,\n value: change.previousValue!, // Use the previous value for the delete\n })\n }\n // If neither matches, don't emit anything\n } else {\n // For deletes, include if the previous value would have matched\n // (so subscribers know something they were tracking was deleted)\n if (filterFn(change.value)) {\n filteredChanges.push(change)\n }\n }\n }\n\n // Always call the original callback if we have filtered changes OR\n // if the original changes array was empty (which indicates a ready signal)\n if (filteredChanges.length > 0 || changes.length === 0) {\n originalCallback(filteredChanges)\n }\n }\n}\n\n/**\n * Gets ordered keys from a collection using index optimization when possible\n * @param collection - The collection to get keys from\n * @param orderBy - The order by clause\n * @param limit - Optional limit on number of keys to return\n * @param whereFilter - Optional filter function to apply while traversing\n * @returns Array of keys in sorted order\n */\nfunction getOrderedKeys<T extends object, TKey extends string | number>(\n collection: CollectionLike<T, TKey>,\n orderBy: OrderBy,\n limit?: number,\n whereFilter?: (item: T) => boolean,\n optimizedOnly?: boolean,\n): Array<TKey> | undefined {\n // For single-column orderBy on a ref expression, try index optimization\n if (orderBy.length === 1) {\n const clause = orderBy[0]!\n const orderByExpression = clause.expression\n\n if (orderByExpression.type === `ref`) {\n const propRef = orderByExpression\n const fieldPath = propRef.path\n const compareOpts = buildCompareOptions(clause, collection)\n\n // Ensure index exists for this field\n ensureIndexForField(\n fieldPath[0]!,\n fieldPath,\n collection as CollectionImpl<T, TKey>,\n compareOpts,\n )\n\n // Find the index\n const index = findIndexForField(collection, fieldPath, compareOpts)\n\n if (index && index.supports(`gt`)) {\n // Use index optimization\n const filterFn = (key: TKey): boolean => {\n const value = collection.get(key)\n if (value === undefined) {\n return false\n }\n return whereFilter?.(value) ?? true\n }\n\n // Take the keys that match the filter and limit\n // if no limit is provided `index.keyCount` is used,\n // i.e. we will take all keys that match the filter\n return index.takeFromStart(limit ?? index.keyCount, filterFn)\n }\n }\n }\n\n if (optimizedOnly) {\n return\n }\n\n // Fallback: collect all items and sort in memory\n const allItems: Array<{ key: TKey; value: T }> = []\n for (const [key, value] of collection.entries()) {\n if (whereFilter?.(value) ?? true) {\n allItems.push({ key, value })\n }\n }\n\n // Sort using makeComparator\n const compare = (a: { key: TKey; value: T }, b: { key: TKey; value: T }) => {\n for (const clause of orderBy) {\n const compareFn = makeComparator(clause.compareOptions)\n\n // Extract values for comparison\n const aValue = extractValueFromItem(a.value, clause.expression)\n const bValue = extractValueFromItem(b.value, clause.expression)\n\n const result = compareFn(aValue, bValue)\n if (result !== 0) {\n return result\n }\n }\n return 0\n }\n\n allItems.sort(compare)\n const sortedKeys = allItems.map((item) => item.key)\n\n // Apply limit if provided\n if (limit !== undefined) {\n return sortedKeys.slice(0, limit)\n }\n\n // if no limit is provided, we will return all keys\n return sortedKeys\n}\n\n/**\n * Helper function to extract a value from an item based on an expression\n */\nfunction extractValueFromItem(item: any, expression: BasicExpression): any {\n if (expression.type === `ref`) {\n const propRef = expression\n let value = item\n for (const pathPart of propRef.path) {\n value = value?.[pathPart]\n }\n return value\n } else if (expression.type === `val`) {\n return expression.value\n } else {\n // It must be a function\n const evaluator = compileSingleRowExpression(expression)\n return evaluator(item as Record<string, unknown>)\n }\n}\n"],"names":[],"mappings":"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"}
@@ -78,7 +78,12 @@ class CollectionChangesManager {
78
78
  subscription.on(`status:change`, options.onStatusChange);
79
79
  }
80
80
  if (options.includeInitialState) {
81
- subscription.requestSnapshot({ trackLoadSubsetPromise: false });
81
+ subscription.requestSnapshot({
82
+ trackLoadSubsetPromise: false,
83
+ orderBy: options.orderBy,
84
+ limit: options.limit,
85
+ onLoadSubsetResult: options.onLoadSubsetResult
86
+ });
82
87
  } else if (options.includeInitialState === false) {
83
88
  subscription.markAllStateAsSeen();
84
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"changes.js","sources":["../../../src/collection/changes.ts"],"sourcesContent":["import { NegativeActiveSubscribersError } from '../errors'\nimport {\n createSingleRowRefProxy,\n toExpression,\n} from '../query/builder/ref-proxy.js'\nimport { CollectionSubscription } from './subscription.js'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { ChangeMessage, SubscribeChangesOptions } from '../types'\nimport type { CollectionLifecycleManager } from './lifecycle.js'\nimport type { CollectionSyncManager } from './sync.js'\nimport type { CollectionEventsManager } from './events.js'\nimport type { CollectionImpl } from './index.js'\n\nexport class CollectionChangesManager<\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 lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n\n public activeSubscribersCount = 0\n public changeSubscriptions = new Set<CollectionSubscription>()\n public batchedEvents: Array<ChangeMessage<TOutput, TKey>> = []\n public shouldBatchEvents = false\n\n /**\n * Creates a new CollectionChangesManager instance\n */\n constructor() {}\n\n public setDeps(deps: {\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n }) {\n this.lifecycle = deps.lifecycle\n this.sync = deps.sync\n this.events = deps.events\n this.collection = deps.collection\n }\n\n /**\n * Emit an empty ready event to notify subscribers that the collection is ready\n * This bypasses the normal empty array check in emitEvents\n */\n public emitEmptyReadyEvent(): void {\n // Emit empty array directly to all subscribers\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents([])\n }\n }\n\n /**\n * Emit events either immediately or batch them for later emission\n */\n public emitEvents(\n changes: Array<ChangeMessage<TOutput, TKey>>,\n forceEmit = false,\n ): void {\n // Skip batching for user actions (forceEmit=true) to keep UI responsive\n if (this.shouldBatchEvents && !forceEmit) {\n // Add events to the batch\n this.batchedEvents.push(...changes)\n return\n }\n\n // Either we're not batching, or we're forcing emission (user action or ending batch cycle)\n let eventsToEmit = changes\n\n if (forceEmit) {\n // Force emit is used to end a batch (e.g. after a sync commit). Combine any\n // buffered optimistic events with the final changes so subscribers see the\n // whole picture, even if the sync diff is empty.\n if (this.batchedEvents.length > 0) {\n eventsToEmit = [...this.batchedEvents, ...changes]\n }\n this.batchedEvents = []\n this.shouldBatchEvents = false\n }\n\n if (eventsToEmit.length === 0) {\n return\n }\n\n // Emit to all listeners\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents(eventsToEmit)\n }\n }\n\n /**\n * Subscribe to changes in the collection\n */\n public subscribeChanges(\n callback: (changes: Array<ChangeMessage<TOutput>>) => void,\n options: SubscribeChangesOptions<TOutput> = {},\n ): CollectionSubscription {\n // Start sync and track subscriber\n this.addSubscriber()\n\n // Compile where callback to whereExpression if provided\n if (options.where && options.whereExpression) {\n throw new Error(\n `Cannot specify both 'where' and 'whereExpression' options. Use one or the other.`,\n )\n }\n\n const { where, ...opts } = options\n let whereExpression = opts.whereExpression\n if (where) {\n const proxy = createSingleRowRefProxy<TOutput>()\n const result = where(proxy)\n whereExpression = toExpression(result)\n }\n\n const subscription = new CollectionSubscription(this.collection, callback, {\n ...opts,\n whereExpression,\n onUnsubscribe: () => {\n this.removeSubscriber()\n this.changeSubscriptions.delete(subscription)\n },\n })\n\n // Register status listener BEFORE requesting snapshot to avoid race condition.\n // This ensures the listener catches all status transitions, even if the\n // loadSubset promise resolves synchronously or very quickly.\n if (options.onStatusChange) {\n subscription.on(`status:change`, options.onStatusChange)\n }\n\n if (options.includeInitialState) {\n subscription.requestSnapshot({ trackLoadSubsetPromise: false })\n } else if (options.includeInitialState === false) {\n // When explicitly set to false (not just undefined), mark all state as \"seen\"\n // so that all future changes (including deletes) pass through unfiltered.\n subscription.markAllStateAsSeen()\n }\n\n // Add to batched listeners\n this.changeSubscriptions.add(subscription)\n\n return subscription\n }\n\n /**\n * Increment the active subscribers count and start sync if needed\n */\n private addSubscriber(): void {\n const previousSubscriberCount = this.activeSubscribersCount\n this.activeSubscribersCount++\n this.lifecycle.cancelGCTimer()\n\n // Start sync if collection was cleaned up\n if (\n this.lifecycle.status === `cleaned-up` ||\n this.lifecycle.status === `idle`\n ) {\n this.sync.startSync()\n }\n\n this.events.emitSubscribersChange(\n this.activeSubscribersCount,\n previousSubscriberCount,\n )\n }\n\n /**\n * Decrement the active subscribers count and start GC timer if needed\n */\n private removeSubscriber(): void {\n const previousSubscriberCount = this.activeSubscribersCount\n this.activeSubscribersCount--\n\n if (this.activeSubscribersCount === 0) {\n this.lifecycle.startGCTimer()\n } else if (this.activeSubscribersCount < 0) {\n throw new NegativeActiveSubscribersError()\n }\n\n this.events.emitSubscribersChange(\n this.activeSubscribersCount,\n previousSubscriberCount,\n )\n }\n\n /**\n * Clean up the collection by stopping sync and clearing data\n * This can be called manually or automatically by garbage collection\n */\n public cleanup(): void {\n this.batchedEvents = []\n this.shouldBatchEvents = false\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"changes.js","sources":["../../../src/collection/changes.ts"],"sourcesContent":["import { NegativeActiveSubscribersError } from '../errors'\nimport {\n createSingleRowRefProxy,\n toExpression,\n} from '../query/builder/ref-proxy.js'\nimport { CollectionSubscription } from './subscription.js'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { ChangeMessage, SubscribeChangesOptions } from '../types'\nimport type { CollectionLifecycleManager } from './lifecycle.js'\nimport type { CollectionSyncManager } from './sync.js'\nimport type { CollectionEventsManager } from './events.js'\nimport type { CollectionImpl } from './index.js'\n\nexport class CollectionChangesManager<\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 lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n\n public activeSubscribersCount = 0\n public changeSubscriptions = new Set<CollectionSubscription>()\n public batchedEvents: Array<ChangeMessage<TOutput, TKey>> = []\n public shouldBatchEvents = false\n\n /**\n * Creates a new CollectionChangesManager instance\n */\n constructor() {}\n\n public setDeps(deps: {\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n }) {\n this.lifecycle = deps.lifecycle\n this.sync = deps.sync\n this.events = deps.events\n this.collection = deps.collection\n }\n\n /**\n * Emit an empty ready event to notify subscribers that the collection is ready\n * This bypasses the normal empty array check in emitEvents\n */\n public emitEmptyReadyEvent(): void {\n // Emit empty array directly to all subscribers\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents([])\n }\n }\n\n /**\n * Emit events either immediately or batch them for later emission\n */\n public emitEvents(\n changes: Array<ChangeMessage<TOutput, TKey>>,\n forceEmit = false,\n ): void {\n // Skip batching for user actions (forceEmit=true) to keep UI responsive\n if (this.shouldBatchEvents && !forceEmit) {\n // Add events to the batch\n this.batchedEvents.push(...changes)\n return\n }\n\n // Either we're not batching, or we're forcing emission (user action or ending batch cycle)\n let eventsToEmit = changes\n\n if (forceEmit) {\n // Force emit is used to end a batch (e.g. after a sync commit). Combine any\n // buffered optimistic events with the final changes so subscribers see the\n // whole picture, even if the sync diff is empty.\n if (this.batchedEvents.length > 0) {\n eventsToEmit = [...this.batchedEvents, ...changes]\n }\n this.batchedEvents = []\n this.shouldBatchEvents = false\n }\n\n if (eventsToEmit.length === 0) {\n return\n }\n\n // Emit to all listeners\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents(eventsToEmit)\n }\n }\n\n /**\n * Subscribe to changes in the collection\n */\n public subscribeChanges(\n callback: (changes: Array<ChangeMessage<TOutput>>) => void,\n options: SubscribeChangesOptions<TOutput> = {},\n ): CollectionSubscription {\n // Start sync and track subscriber\n this.addSubscriber()\n\n // Compile where callback to whereExpression if provided\n if (options.where && options.whereExpression) {\n throw new Error(\n `Cannot specify both 'where' and 'whereExpression' options. Use one or the other.`,\n )\n }\n\n const { where, ...opts } = options\n let whereExpression = opts.whereExpression\n if (where) {\n const proxy = createSingleRowRefProxy<TOutput>()\n const result = where(proxy)\n whereExpression = toExpression(result)\n }\n\n const subscription = new CollectionSubscription(this.collection, callback, {\n ...opts,\n whereExpression,\n onUnsubscribe: () => {\n this.removeSubscriber()\n this.changeSubscriptions.delete(subscription)\n },\n })\n\n // Register status listener BEFORE requesting snapshot to avoid race condition.\n // This ensures the listener catches all status transitions, even if the\n // loadSubset promise resolves synchronously or very quickly.\n if (options.onStatusChange) {\n subscription.on(`status:change`, options.onStatusChange)\n }\n\n if (options.includeInitialState) {\n subscription.requestSnapshot({\n trackLoadSubsetPromise: false,\n orderBy: options.orderBy,\n limit: options.limit,\n onLoadSubsetResult: options.onLoadSubsetResult,\n })\n } else if (options.includeInitialState === false) {\n // When explicitly set to false (not just undefined), mark all state as \"seen\"\n // so that all future changes (including deletes) pass through unfiltered.\n subscription.markAllStateAsSeen()\n }\n\n // Add to batched listeners\n this.changeSubscriptions.add(subscription)\n\n return subscription\n }\n\n /**\n * Increment the active subscribers count and start sync if needed\n */\n private addSubscriber(): void {\n const previousSubscriberCount = this.activeSubscribersCount\n this.activeSubscribersCount++\n this.lifecycle.cancelGCTimer()\n\n // Start sync if collection was cleaned up\n if (\n this.lifecycle.status === `cleaned-up` ||\n this.lifecycle.status === `idle`\n ) {\n this.sync.startSync()\n }\n\n this.events.emitSubscribersChange(\n this.activeSubscribersCount,\n previousSubscriberCount,\n )\n }\n\n /**\n * Decrement the active subscribers count and start GC timer if needed\n */\n private removeSubscriber(): void {\n const previousSubscriberCount = this.activeSubscribersCount\n this.activeSubscribersCount--\n\n if (this.activeSubscribersCount === 0) {\n this.lifecycle.startGCTimer()\n } else if (this.activeSubscribersCount < 0) {\n throw new NegativeActiveSubscribersError()\n }\n\n this.events.emitSubscribersChange(\n this.activeSubscribersCount,\n previousSubscriberCount,\n )\n }\n\n /**\n * Clean up the collection by stopping sync and clearing data\n * This can be called manually or automatically by garbage collection\n */\n public cleanup(): void {\n this.batchedEvents = []\n this.shouldBatchEvents = false\n }\n}\n"],"names":[],"mappings":"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"}
@@ -169,7 +169,18 @@ class CollectionLifecycleManager {
169
169
  this.gcTimeoutId = null;
170
170
  }
171
171
  this.hasBeenReady = false;
172
+ const callbacks = [...this.onFirstReadyCallbacks];
172
173
  this.onFirstReadyCallbacks = [];
174
+ callbacks.forEach((callback) => {
175
+ try {
176
+ callback();
177
+ } catch (error) {
178
+ console.error(
179
+ `${this.config.id ? `[${this.config.id}] ` : ``}Error in onFirstReady callback during cleanup:`,
180
+ error
181
+ );
182
+ }
183
+ });
173
184
  this.setStatus(`cleaned-up`);
174
185
  this.events.cleanup();
175
186
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.js","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n CollectionStateError,\n InvalidCollectionStatusTransitionError,\n} from '../errors'\nimport {\n safeCancelIdleCallback,\n safeRequestIdleCallback,\n} from '../utils/browser-polyfills'\nimport type { IdleCallbackDeadline } from '../utils/browser-polyfills'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { CollectionConfig, CollectionStatus } from '../types'\nimport type { CollectionEventsManager } from './events'\nimport type { CollectionIndexesManager } from './indexes'\nimport type { CollectionChangesManager } from './changes'\nimport type { CollectionSyncManager } from './sync'\nimport type { CollectionStateManager } from './state'\n\nexport class CollectionLifecycleManager<\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 config: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n private indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n public status: CollectionStatus = `idle`\n public hasBeenReady = false\n public hasReceivedFirstCommit = false\n public onFirstReadyCallbacks: Array<() => void> = []\n public gcTimeoutId: ReturnType<typeof setTimeout> | null = null\n private idleCallbackId: number | null = null\n\n /**\n * Creates a new CollectionLifecycleManager 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 indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.indexes = deps.indexes\n this.events = deps.events\n this.changes = deps.changes\n this.sync = deps.sync\n this.state = deps.state\n }\n\n /**\n * Validates state transitions to prevent invalid status changes\n */\n public validateStatusTransition(\n from: CollectionStatus,\n to: CollectionStatus,\n ): void {\n if (from === to) {\n // Allow same state transitions\n return\n }\n const validTransitions: Record<\n CollectionStatus,\n Array<CollectionStatus>\n > = {\n idle: [`loading`, `error`, `cleaned-up`],\n loading: [`ready`, `error`, `cleaned-up`],\n ready: [`cleaned-up`, `error`],\n error: [`cleaned-up`, `idle`],\n 'cleaned-up': [`loading`, `error`],\n }\n\n if (!validTransitions[from].includes(to)) {\n throw new InvalidCollectionStatusTransitionError(from, to, this.id)\n }\n }\n\n /**\n * Safely update the collection status with validation\n * @private\n */\n public setStatus(\n newStatus: CollectionStatus,\n allowReady: boolean = false,\n ): void {\n if (newStatus === `ready` && !allowReady) {\n // setStatus('ready') is an internal method that should not be called directly\n // Instead, use markReady to transition to ready triggering the necessary events\n // and side effects.\n throw new CollectionStateError(\n `You can't directly call \"setStatus('ready'). You must use markReady instead.`,\n )\n }\n this.validateStatusTransition(this.status, newStatus)\n const previousStatus = this.status\n this.status = newStatus\n\n // Resolve indexes when collection becomes ready\n if (newStatus === `ready` && !this.indexes.isIndexesResolved) {\n // Resolve indexes asynchronously without blocking\n this.indexes.resolveAllIndexes().catch((error) => {\n console.warn(\n `${this.config.id ? `[${this.config.id}] ` : ``}Failed to resolve indexes:`,\n error,\n )\n })\n }\n\n // Emit event\n this.events.emitStatusChange(newStatus, previousStatus)\n }\n\n /**\n * Validates that the collection is in a usable state for data operations\n * @private\n */\n public validateCollectionUsable(operation: string): void {\n switch (this.status) {\n case `error`:\n throw new CollectionInErrorStateError(operation, this.id)\n case `cleaned-up`:\n // Automatically restart the collection when operations are called on cleaned-up collections\n this.sync.startSync()\n break\n }\n }\n\n /**\n * Mark the collection as ready for use\n * This is called by sync implementations to explicitly signal that the collection is ready,\n * providing a more intuitive alternative to using commits for readiness signaling\n * @private - Should only be called by sync implementations\n */\n public markReady(): void {\n this.validateStatusTransition(this.status, `ready`)\n // Can transition to ready from loading state\n if (this.status === `loading`) {\n this.setStatus(`ready`, true)\n\n // Call any registered first ready callbacks (only on first time becoming ready)\n if (!this.hasBeenReady) {\n this.hasBeenReady = true\n\n // Also mark as having received first commit for backwards compatibility\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n }\n\n const callbacks = [...this.onFirstReadyCallbacks]\n this.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n // Notify dependents when markReady is called, after status is set\n // This ensures live queries get notified when their dependencies become ready\n if (this.changes.changeSubscriptions.size > 0) {\n this.changes.emitEmptyReadyEvent()\n }\n }\n }\n\n /**\n * Start the garbage collection timer\n * Called when the collection becomes inactive (no subscribers)\n */\n public startGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n }\n\n const gcTime = this.config.gcTime ?? 300000 // 5 minutes default\n\n // If gcTime is 0, negative, or non-finite (Infinity, -Infinity, NaN), GC is disabled.\n // Note: setTimeout with Infinity coerces to 0 via ToInt32, causing immediate GC,\n // so we must explicitly check for non-finite values here.\n if (gcTime <= 0 || !Number.isFinite(gcTime)) {\n return\n }\n\n this.gcTimeoutId = setTimeout(() => {\n if (this.changes.activeSubscribersCount === 0) {\n // Schedule cleanup during idle time to avoid blocking the UI thread\n this.scheduleIdleCleanup()\n }\n }, gcTime)\n }\n\n /**\n * Cancel the garbage collection timer\n * Called when the collection becomes active again\n */\n public cancelGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n // Also cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n }\n\n /**\n * Schedule cleanup to run during browser idle time\n * This prevents blocking the UI thread during cleanup operations\n */\n private scheduleIdleCleanup(): void {\n // Cancel any existing idle callback\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n }\n\n // Schedule cleanup with a timeout of 1 second\n // This ensures cleanup happens even if the browser is busy\n this.idleCallbackId = safeRequestIdleCallback(\n (deadline) => {\n // Perform cleanup if we still have no subscribers\n if (this.changes.activeSubscribersCount === 0) {\n const cleanupCompleted = this.performCleanup(deadline)\n // Only clear the callback ID if cleanup actually completed\n if (cleanupCompleted) {\n this.idleCallbackId = null\n }\n } else {\n // No need to cleanup, clear the callback ID\n this.idleCallbackId = null\n }\n },\n { timeout: 1000 },\n )\n }\n\n /**\n * Perform cleanup operations, optionally in chunks during idle time\n * @returns true if cleanup was completed, false if it was rescheduled\n */\n private performCleanup(deadline?: IdleCallbackDeadline): boolean {\n // If we have a deadline, we can potentially split cleanup into chunks\n // For now, we'll do all cleanup at once but check if we have time\n const hasTime =\n !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout\n\n if (hasTime) {\n // Perform all cleanup operations except events\n this.sync.cleanup()\n this.state.cleanup()\n this.changes.cleanup()\n this.indexes.cleanup()\n\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n\n this.hasBeenReady = false\n this.onFirstReadyCallbacks = []\n\n // Set status to cleaned-up after everything is cleaned up\n // This fires the status:change event to notify listeners\n this.setStatus(`cleaned-up`)\n\n // Finally, cleanup event handlers after the event has been fired\n this.events.cleanup()\n\n return true\n } else {\n // If we don't have time, reschedule for the next idle period\n this.scheduleIdleCleanup()\n return false\n }\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n */\n public onFirstReady(callback: () => void): void {\n // If already ready, call immediately\n if (this.hasBeenReady) {\n callback()\n return\n }\n\n this.onFirstReadyCallbacks.push(callback)\n }\n\n public cleanup(): void {\n // Cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n\n // Perform cleanup immediately (used when explicitly called)\n this.performCleanup()\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle.js","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n CollectionStateError,\n InvalidCollectionStatusTransitionError,\n} from '../errors'\nimport {\n safeCancelIdleCallback,\n safeRequestIdleCallback,\n} from '../utils/browser-polyfills'\nimport type { IdleCallbackDeadline } from '../utils/browser-polyfills'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { CollectionConfig, CollectionStatus } from '../types'\nimport type { CollectionEventsManager } from './events'\nimport type { CollectionIndexesManager } from './indexes'\nimport type { CollectionChangesManager } from './changes'\nimport type { CollectionSyncManager } from './sync'\nimport type { CollectionStateManager } from './state'\n\nexport class CollectionLifecycleManager<\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 config: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n private indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n public status: CollectionStatus = `idle`\n public hasBeenReady = false\n public hasReceivedFirstCommit = false\n public onFirstReadyCallbacks: Array<() => void> = []\n public gcTimeoutId: ReturnType<typeof setTimeout> | null = null\n private idleCallbackId: number | null = null\n\n /**\n * Creates a new CollectionLifecycleManager 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 indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.indexes = deps.indexes\n this.events = deps.events\n this.changes = deps.changes\n this.sync = deps.sync\n this.state = deps.state\n }\n\n /**\n * Validates state transitions to prevent invalid status changes\n */\n public validateStatusTransition(\n from: CollectionStatus,\n to: CollectionStatus,\n ): void {\n if (from === to) {\n // Allow same state transitions\n return\n }\n const validTransitions: Record<\n CollectionStatus,\n Array<CollectionStatus>\n > = {\n idle: [`loading`, `error`, `cleaned-up`],\n loading: [`ready`, `error`, `cleaned-up`],\n ready: [`cleaned-up`, `error`],\n error: [`cleaned-up`, `idle`],\n 'cleaned-up': [`loading`, `error`],\n }\n\n if (!validTransitions[from].includes(to)) {\n throw new InvalidCollectionStatusTransitionError(from, to, this.id)\n }\n }\n\n /**\n * Safely update the collection status with validation\n * @private\n */\n public setStatus(\n newStatus: CollectionStatus,\n allowReady: boolean = false,\n ): void {\n if (newStatus === `ready` && !allowReady) {\n // setStatus('ready') is an internal method that should not be called directly\n // Instead, use markReady to transition to ready triggering the necessary events\n // and side effects.\n throw new CollectionStateError(\n `You can't directly call \"setStatus('ready'). You must use markReady instead.`,\n )\n }\n this.validateStatusTransition(this.status, newStatus)\n const previousStatus = this.status\n this.status = newStatus\n\n // Resolve indexes when collection becomes ready\n if (newStatus === `ready` && !this.indexes.isIndexesResolved) {\n // Resolve indexes asynchronously without blocking\n this.indexes.resolveAllIndexes().catch((error) => {\n console.warn(\n `${this.config.id ? `[${this.config.id}] ` : ``}Failed to resolve indexes:`,\n error,\n )\n })\n }\n\n // Emit event\n this.events.emitStatusChange(newStatus, previousStatus)\n }\n\n /**\n * Validates that the collection is in a usable state for data operations\n * @private\n */\n public validateCollectionUsable(operation: string): void {\n switch (this.status) {\n case `error`:\n throw new CollectionInErrorStateError(operation, this.id)\n case `cleaned-up`:\n // Automatically restart the collection when operations are called on cleaned-up collections\n this.sync.startSync()\n break\n }\n }\n\n /**\n * Mark the collection as ready for use\n * This is called by sync implementations to explicitly signal that the collection is ready,\n * providing a more intuitive alternative to using commits for readiness signaling\n * @private - Should only be called by sync implementations\n */\n public markReady(): void {\n this.validateStatusTransition(this.status, `ready`)\n // Can transition to ready from loading state\n if (this.status === `loading`) {\n this.setStatus(`ready`, true)\n\n // Call any registered first ready callbacks (only on first time becoming ready)\n if (!this.hasBeenReady) {\n this.hasBeenReady = true\n\n // Also mark as having received first commit for backwards compatibility\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n }\n\n const callbacks = [...this.onFirstReadyCallbacks]\n this.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n // Notify dependents when markReady is called, after status is set\n // This ensures live queries get notified when their dependencies become ready\n if (this.changes.changeSubscriptions.size > 0) {\n this.changes.emitEmptyReadyEvent()\n }\n }\n }\n\n /**\n * Start the garbage collection timer\n * Called when the collection becomes inactive (no subscribers)\n */\n public startGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n }\n\n const gcTime = this.config.gcTime ?? 300000 // 5 minutes default\n\n // If gcTime is 0, negative, or non-finite (Infinity, -Infinity, NaN), GC is disabled.\n // Note: setTimeout with Infinity coerces to 0 via ToInt32, causing immediate GC,\n // so we must explicitly check for non-finite values here.\n if (gcTime <= 0 || !Number.isFinite(gcTime)) {\n return\n }\n\n this.gcTimeoutId = setTimeout(() => {\n if (this.changes.activeSubscribersCount === 0) {\n // Schedule cleanup during idle time to avoid blocking the UI thread\n this.scheduleIdleCleanup()\n }\n }, gcTime)\n }\n\n /**\n * Cancel the garbage collection timer\n * Called when the collection becomes active again\n */\n public cancelGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n // Also cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n }\n\n /**\n * Schedule cleanup to run during browser idle time\n * This prevents blocking the UI thread during cleanup operations\n */\n private scheduleIdleCleanup(): void {\n // Cancel any existing idle callback\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n }\n\n // Schedule cleanup with a timeout of 1 second\n // This ensures cleanup happens even if the browser is busy\n this.idleCallbackId = safeRequestIdleCallback(\n (deadline) => {\n // Perform cleanup if we still have no subscribers\n if (this.changes.activeSubscribersCount === 0) {\n const cleanupCompleted = this.performCleanup(deadline)\n // Only clear the callback ID if cleanup actually completed\n if (cleanupCompleted) {\n this.idleCallbackId = null\n }\n } else {\n // No need to cleanup, clear the callback ID\n this.idleCallbackId = null\n }\n },\n { timeout: 1000 },\n )\n }\n\n /**\n * Perform cleanup operations, optionally in chunks during idle time\n * @returns true if cleanup was completed, false if it was rescheduled\n */\n private performCleanup(deadline?: IdleCallbackDeadline): boolean {\n // If we have a deadline, we can potentially split cleanup into chunks\n // For now, we'll do all cleanup at once but check if we have time\n const hasTime =\n !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout\n\n if (hasTime) {\n // Perform all cleanup operations except events\n this.sync.cleanup()\n this.state.cleanup()\n this.changes.cleanup()\n this.indexes.cleanup()\n\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n\n this.hasBeenReady = false\n\n // Call any pending onFirstReady callbacks before clearing them.\n // This ensures preload() promises resolve during cleanup instead of hanging.\n const callbacks = [...this.onFirstReadyCallbacks]\n this.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => {\n try {\n callback()\n } catch (error) {\n console.error(\n `${this.config.id ? `[${this.config.id}] ` : ``}Error in onFirstReady callback during cleanup:`,\n error,\n )\n }\n })\n\n // Set status to cleaned-up after everything is cleaned up\n // This fires the status:change event to notify listeners\n this.setStatus(`cleaned-up`)\n\n // Finally, cleanup event handlers after the event has been fired\n this.events.cleanup()\n\n return true\n } else {\n // If we don't have time, reschedule for the next idle period\n this.scheduleIdleCleanup()\n return false\n }\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n */\n public onFirstReady(callback: () => void): void {\n // If already ready, call immediately\n if (this.hasBeenReady) {\n callback()\n return\n }\n\n this.onFirstReadyCallbacks.push(callback)\n }\n\n public cleanup(): void {\n // Cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n\n // Perform cleanup immediately (used when explicitly called)\n this.performCleanup()\n }\n}\n"],"names":[],"mappings":"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"}
@@ -11,6 +11,8 @@ type RequestSnapshotOptions = {
11
11
  orderBy?: OrderBy;
12
12
  /** Optional limit to pass to loadSubset for backend optimization */
13
13
  limit?: number;
14
+ /** Callback that receives the raw loadSubset result for external tracking */
15
+ onLoadSubsetResult?: (result: Promise<void> | true) => void;
14
16
  };
15
17
  type RequestLimitedSnapshotOptions = {
16
18
  orderBy: OrderBy;
@@ -19,6 +21,10 @@ type RequestLimitedSnapshotOptions = {
19
21
  minValues?: Array<unknown>;
20
22
  /** Row offset for offset-based pagination (passed to sync layer) */
21
23
  offset?: number;
24
+ /** Whether to track the loadSubset promise on this subscription (default: true) */
25
+ trackLoadSubsetPromise?: boolean;
26
+ /** Callback that receives the raw loadSubset result for external tracking */
27
+ onLoadSubsetResult?: (result: Promise<void> | true) => void;
22
28
  };
23
29
  type CollectionSubscriptionOptions = {
24
30
  includeInitialState?: boolean;
@@ -101,7 +107,7 @@ export declare class CollectionSubscription extends EventEmitter<SubscriptionEve
101
107
  * This is needed to ensure that it does not accidentally skip duplicate values when the limit falls in the middle of some duplicated values.
102
108
  * Note 2: it does not send keys that have already been sent before.
103
109
  */
104
- requestLimitedSnapshot({ orderBy, limit, minValues, offset, }: RequestLimitedSnapshotOptions): void;
110
+ requestLimitedSnapshot({ orderBy, limit, minValues, offset, trackLoadSubsetPromise: shouldTrackLoadSubsetPromise, onLoadSubsetResult, }: RequestLimitedSnapshotOptions): void;
105
111
  /**
106
112
  * Filters and flips changes for keys that have not been sent yet.
107
113
  * Deletes are filtered out for keys that have not been sent yet.
@@ -202,6 +202,7 @@ class CollectionSubscription extends EventEmitter {
202
202
  limit: opts?.limit
203
203
  };
204
204
  const syncResult = this.collection._sync.loadSubset(loadOptions);
205
+ opts?.onLoadSubsetResult?.(syncResult);
205
206
  this.loadedSubsets.push(loadOptions);
206
207
  const trackLoadSubsetPromise = opts?.trackLoadSubsetPromise ?? true;
207
208
  if (trackLoadSubsetPromise) {
@@ -238,7 +239,9 @@ class CollectionSubscription extends EventEmitter {
238
239
  orderBy,
239
240
  limit,
240
241
  minValues,
241
- offset
242
+ offset,
243
+ trackLoadSubsetPromise: shouldTrackLoadSubsetPromise = true,
244
+ onLoadSubsetResult
242
245
  }) {
243
246
  if (!limit) throw new Error(`limit is required`);
244
247
  if (!this.orderByIndex) {
@@ -246,13 +249,14 @@ class CollectionSubscription extends EventEmitter {
246
249
  `Ordered snapshot was requested but no index was found. You have to call setOrderByIndex before requesting an ordered snapshot.`
247
250
  );
248
251
  }
252
+ const hasMinValue = minValues !== void 0 && minValues.length > 0;
249
253
  const minValue = minValues?.[0];
250
254
  const minValueForIndex = minValue;
251
255
  const index = this.orderByIndex;
252
256
  const where = this.options.whereExpression;
253
257
  const whereFilterFn = where ? createFilterFunctionFromExpression(where) : void 0;
254
258
  const filterFn = (key) => {
255
- if (this.sentKeys.has(key)) {
259
+ if (key !== void 0 && this.sentKeys.has(key)) {
256
260
  return false;
257
261
  }
258
262
  const value = this.collection.get(key);
@@ -264,7 +268,7 @@ class CollectionSubscription extends EventEmitter {
264
268
  let biggestObservedValue = minValueForIndex;
265
269
  const changes = [];
266
270
  let keys = [];
267
- if (minValueForIndex !== void 0) {
271
+ if (hasMinValue) {
268
272
  const { expression } = orderBy[0];
269
273
  const allRowsWithMinValue = this.collection.currentStateAsChanges({
270
274
  where: eq(expression, new Value(minValueForIndex))
@@ -282,7 +286,7 @@ class CollectionSubscription extends EventEmitter {
282
286
  keys = index.take(limit, minValueForIndex, filterFn);
283
287
  }
284
288
  } else {
285
- keys = index.take(limit, minValueForIndex, filterFn);
289
+ keys = index.takeFromStart(limit, filterFn);
286
290
  }
287
291
  const valuesNeeded = () => Math.max(limit - changes.length, 0);
288
292
  const collectionExhausted = () => keys.length === 0;
@@ -346,8 +350,11 @@ class CollectionSubscription extends EventEmitter {
346
350
  subscription: this
347
351
  };
348
352
  const syncResult = this.collection._sync.loadSubset(loadOptions);
353
+ onLoadSubsetResult?.(syncResult);
349
354
  this.loadedSubsets.push(loadOptions);
350
- this.trackLoadSubsetPromise(syncResult);
355
+ if (shouldTrackLoadSubsetPromise) {
356
+ this.trackLoadSubsetPromise(syncResult);
357
+ }
351
358
  }
352
359
  // TODO: also add similar test but that checks that it can also load it from the collection's loadSubset function
353
360
  // and that that also works properly (i.e. does not skip duplicate values)
@@ -397,6 +404,12 @@ class CollectionSubscription extends EventEmitter {
397
404
  this.sentKeys.add(change.key);
398
405
  }
399
406
  }
407
+ if (this.orderByIndex) {
408
+ this.limitedSnapshotRowCount = Math.max(
409
+ this.limitedSnapshotRowCount,
410
+ this.sentKeys.size
411
+ );
412
+ }
400
413
  }
401
414
  /**
402
415
  * Mark that the subscription should not filter any changes.