@tanstack/db 0.5.3 → 0.5.5
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.
- package/README.md +10 -10
- package/dist/cjs/collection/subscription.cjs +17 -6
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +5 -0
- package/dist/cjs/collection/sync.cjs +16 -0
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +6 -0
- package/dist/cjs/errors.cjs +8 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.cjs +18 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/strategies/debounceStrategy.cjs +4 -4
- package/dist/cjs/strategies/debounceStrategy.cjs.map +1 -1
- package/dist/cjs/strategies/queueStrategy.cjs +10 -8
- package/dist/cjs/strategies/queueStrategy.cjs.map +1 -1
- package/dist/cjs/strategies/throttleStrategy.cjs +4 -4
- package/dist/cjs/strategies/throttleStrategy.cjs.map +1 -1
- package/dist/cjs/types.d.cts +2 -0
- package/dist/esm/collection/subscription.d.ts +5 -0
- package/dist/esm/collection/subscription.js +17 -6
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +6 -0
- package/dist/esm/collection/sync.js +16 -0
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +8 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/query/builder/index.js +19 -3
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/strategies/debounceStrategy.js +2 -2
- package/dist/esm/strategies/debounceStrategy.js.map +1 -1
- package/dist/esm/strategies/queueStrategy.js +10 -8
- package/dist/esm/strategies/queueStrategy.js.map +1 -1
- package/dist/esm/strategies/throttleStrategy.js +2 -2
- package/dist/esm/strategies/throttleStrategy.js.map +1 -1
- package/dist/esm/types.d.ts +2 -0
- package/package.json +2 -2
- package/src/collection/subscription.ts +32 -6
- package/src/collection/sync.ts +25 -0
- package/src/errors.ts +9 -0
- package/src/query/builder/index.ts +28 -2
- package/src/strategies/debounceStrategy.ts +2 -2
- package/src/strategies/queueStrategy.ts +22 -9
- package/src/strategies/throttleStrategy.ts +2 -2
- package/src/types.ts +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../../../src/query/builder/index.ts"],"sourcesContent":["import { CollectionImpl } from \"../../collection/index.js\"\nimport {\n Aggregate as AggregateExpr,\n CollectionRef,\n Func as FuncExpr,\n PropRef,\n QueryRef,\n Value as ValueExpr,\n isExpressionLike,\n} from \"../ir.js\"\nimport {\n InvalidSourceError,\n JoinConditionMustBeEqualityError,\n OnlyOneSourceAllowedError,\n QueryMustHaveFromClauseError,\n SubQueryMustHaveFromClauseError,\n} from \"../../errors.js\"\nimport { createRefProxy, toExpression } from \"./ref-proxy.js\"\nimport type { NamespacedRow, SingleResult } from \"../../types.js\"\nimport type {\n Aggregate,\n BasicExpression,\n JoinClause,\n OrderBy,\n OrderByDirection,\n QueryIR,\n} from \"../ir.js\"\nimport type {\n CompareOptions,\n Context,\n GroupByCallback,\n JoinOnCallback,\n MergeContextForJoinCallback,\n MergeContextWithJoinType,\n OrderByCallback,\n OrderByOptions,\n RefsForContext,\n ResultTypeFromSelect,\n SchemaFromSource,\n SelectObject,\n Source,\n WhereCallback,\n WithResult,\n} from \"./types.js\"\n\nexport class BaseQueryBuilder<TContext extends Context = Context> {\n private readonly query: Partial<QueryIR> = {}\n\n constructor(query: Partial<QueryIR> = {}) {\n this.query = { ...query }\n }\n\n /**\n * Creates a CollectionRef or QueryRef from a source object\n * @param source - An object with a single key-value pair\n * @param context - Context string for error messages (e.g., \"from clause\", \"join clause\")\n * @returns A tuple of [alias, ref] where alias is the source key and ref is the created reference\n */\n private _createRefForSource<TSource extends Source>(\n source: TSource,\n context: string\n ): [string, CollectionRef | QueryRef] {\n if (Object.keys(source).length !== 1) {\n throw new OnlyOneSourceAllowedError(context)\n }\n\n const alias = Object.keys(source)[0]!\n const sourceValue = source[alias]\n\n let ref: CollectionRef | QueryRef\n\n if (sourceValue instanceof CollectionImpl) {\n ref = new CollectionRef(sourceValue, alias)\n } else if (sourceValue instanceof BaseQueryBuilder) {\n const subQuery = sourceValue._getQuery()\n if (!(subQuery as Partial<QueryIR>).from) {\n throw new SubQueryMustHaveFromClauseError(context)\n }\n ref = new QueryRef(subQuery, alias)\n } else {\n throw new InvalidSourceError(alias)\n }\n\n return [alias, ref]\n }\n\n /**\n * Specify the source table or subquery for the query\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @returns A QueryBuilder with the specified source\n *\n * @example\n * ```ts\n * // Query from a collection\n * query.from({ users: usersCollection })\n *\n * // Query from a subquery\n * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)\n * query.from({ activeUsers })\n * ```\n */\n from<TSource extends Source>(\n source: TSource\n ): QueryBuilder<{\n baseSchema: SchemaFromSource<TSource>\n schema: SchemaFromSource<TSource>\n fromSourceName: keyof TSource & string\n hasJoins: false\n }> {\n const [, from] = this._createRefForSource(source, `from clause`)\n\n return new BaseQueryBuilder({\n ...this.query,\n from,\n }) as any\n }\n\n /**\n * Join another table or subquery to the current query\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @param type - The type of join: 'inner', 'left', 'right', or 'full' (defaults to 'left')\n * @returns A QueryBuilder with the joined table available\n *\n * @example\n * ```ts\n * // Left join users with posts\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n *\n * // Inner join with explicit type\n * query\n * .from({ u: usersCollection })\n * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId), 'inner')\n * ```\n *\n * // Join with a subquery\n * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)\n * query\n * .from({ activeUsers })\n * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId))\n */\n join<\n TSource extends Source,\n TJoinType extends `inner` | `left` | `right` | `full` = `left`,\n >(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >,\n type: TJoinType = `left` as TJoinType\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, TJoinType>\n > {\n const [alias, from] = this._createRefForSource(source, `join clause`)\n\n // Create a temporary context for the callback\n const currentAliases = this._getCurrentAliases()\n const newAliases = [...currentAliases, alias]\n const refProxy = createRefProxy(newAliases) as RefsForContext<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n\n // Get the join condition expression\n const onExpression = onCallback(refProxy)\n\n // Extract left and right from the expression\n // For now, we'll assume it's an eq function with two arguments\n let left: BasicExpression\n let right: BasicExpression\n\n if (\n onExpression.type === `func` &&\n onExpression.name === `eq` &&\n onExpression.args.length === 2\n ) {\n left = onExpression.args[0]!\n right = onExpression.args[1]!\n } else {\n throw new JoinConditionMustBeEqualityError()\n }\n\n const joinClause: JoinClause = {\n from,\n type,\n left,\n right,\n }\n\n const existingJoins = this.query.join || []\n\n return new BaseQueryBuilder({\n ...this.query,\n join: [...existingJoins, joinClause],\n }) as any\n }\n\n /**\n * Perform a LEFT JOIN with another table or subquery\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @returns A QueryBuilder with the left joined table available\n *\n * @example\n * ```ts\n * // Left join users with posts\n * query\n * .from({ users: usersCollection })\n * .leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n * ```\n */\n leftJoin<TSource extends Source>(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `left`>\n > {\n return this.join(source, onCallback, `left`)\n }\n\n /**\n * Perform a RIGHT JOIN with another table or subquery\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @returns A QueryBuilder with the right joined table available\n *\n * @example\n * ```ts\n * // Right join users with posts\n * query\n * .from({ users: usersCollection })\n * .rightJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n * ```\n */\n rightJoin<TSource extends Source>(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `right`>\n > {\n return this.join(source, onCallback, `right`)\n }\n\n /**\n * Perform an INNER JOIN with another table or subquery\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @returns A QueryBuilder with the inner joined table available\n *\n * @example\n * ```ts\n * // Inner join users with posts\n * query\n * .from({ users: usersCollection })\n * .innerJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n * ```\n */\n innerJoin<TSource extends Source>(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `inner`>\n > {\n return this.join(source, onCallback, `inner`)\n }\n\n /**\n * Perform a FULL JOIN with another table or subquery\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @returns A QueryBuilder with the full joined table available\n *\n * @example\n * ```ts\n * // Full join users with posts\n * query\n * .from({ users: usersCollection })\n * .fullJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n * ```\n */\n fullJoin<TSource extends Source>(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `full`>\n > {\n return this.join(source, onCallback, `full`)\n }\n\n /**\n * Filter rows based on a condition\n *\n * @param callback - A function that receives table references and returns an expression\n * @returns A QueryBuilder with the where condition applied\n *\n * @example\n * ```ts\n * // Simple condition\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => gt(users.age, 18))\n *\n * // Multiple conditions\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => and(\n * gt(users.age, 18),\n * eq(users.active, true)\n * ))\n *\n * // Multiple where calls are ANDed together\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => gt(users.age, 18))\n * .where(({users}) => eq(users.active, true))\n * ```\n */\n where(callback: WhereCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const expression = callback(refProxy)\n\n const existingWhere = this.query.where || []\n\n return new BaseQueryBuilder({\n ...this.query,\n where: [...existingWhere, expression],\n }) as any\n }\n\n /**\n * Filter grouped rows based on aggregate conditions\n *\n * @param callback - A function that receives table references and returns an expression\n * @returns A QueryBuilder with the having condition applied\n *\n * @example\n * ```ts\n * // Filter groups by count\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .having(({posts}) => gt(count(posts.id), 5))\n *\n * // Filter by average\n * query\n * .from({ orders: ordersCollection })\n * .groupBy(({orders}) => orders.customerId)\n * .having(({orders}) => gt(avg(orders.total), 100))\n *\n * // Multiple having calls are ANDed together\n * query\n * .from({ orders: ordersCollection })\n * .groupBy(({orders}) => orders.customerId)\n * .having(({orders}) => gt(count(orders.id), 5))\n * .having(({orders}) => gt(avg(orders.total), 100))\n * ```\n */\n having(callback: WhereCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const expression = callback(refProxy)\n\n const existingHaving = this.query.having || []\n\n return new BaseQueryBuilder({\n ...this.query,\n having: [...existingHaving, expression],\n }) as any\n }\n\n /**\n * Select specific columns or computed values from the query\n *\n * @param callback - A function that receives table references and returns an object with selected fields or expressions\n * @returns A QueryBuilder that returns only the selected fields\n *\n * @example\n * ```ts\n * // Select specific columns\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => ({\n * name: users.name,\n * email: users.email\n * }))\n *\n * // Select with computed values\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => ({\n * fullName: concat(users.firstName, ' ', users.lastName),\n * ageInMonths: mul(users.age, 12)\n * }))\n *\n * // Select with aggregates (requires GROUP BY)\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .select(({posts, count}) => ({\n * userId: posts.userId,\n * postCount: count(posts.id)\n * }))\n * ```\n */\n select<TSelectObject extends SelectObject>(\n callback: (refs: RefsForContext<TContext>) => TSelectObject\n ): QueryBuilder<WithResult<TContext, ResultTypeFromSelect<TSelectObject>>> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const selectObject = callback(refProxy)\n const select = buildNestedSelect(selectObject)\n\n return new BaseQueryBuilder({\n ...this.query,\n select: select,\n fnSelect: undefined, // remove the fnSelect clause if it exists\n }) as any\n }\n\n /**\n * Sort the query results by one or more columns\n *\n * @param callback - A function that receives table references and returns the field to sort by\n * @param direction - Sort direction: 'asc' for ascending, 'desc' for descending (defaults to 'asc')\n * @returns A QueryBuilder with the ordering applied\n *\n * @example\n * ```ts\n * // Sort by a single column\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.name)\n *\n * // Sort descending\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.createdAt, 'desc')\n *\n * // Multiple sorts (chain orderBy calls)\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.lastName)\n * .orderBy(({users}) => users.firstName)\n * ```\n */\n orderBy(\n callback: OrderByCallback<TContext>,\n options: OrderByDirection | OrderByOptions = `asc`\n ): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const result = callback(refProxy)\n\n const opts: CompareOptions =\n typeof options === `string`\n ? { direction: options, nulls: `first` }\n : {\n direction: options.direction ?? `asc`,\n nulls: options.nulls ?? `first`,\n stringSort: options.stringSort,\n locale:\n options.stringSort === `locale` ? options.locale : undefined,\n localeOptions:\n options.stringSort === `locale`\n ? options.localeOptions\n : undefined,\n }\n\n const makeOrderByClause = (res: any) => {\n return {\n expression: toExpression(res),\n compareOptions: opts,\n }\n }\n\n // Create the new OrderBy structure with expression and direction\n const orderByClauses = Array.isArray(result)\n ? result.map((r) => makeOrderByClause(r))\n : [makeOrderByClause(result)]\n\n const existingOrderBy: OrderBy = this.query.orderBy || []\n\n return new BaseQueryBuilder({\n ...this.query,\n orderBy: [...existingOrderBy, ...orderByClauses],\n }) as any\n }\n\n /**\n * Group rows by one or more columns for aggregation\n *\n * @param callback - A function that receives table references and returns the field(s) to group by\n * @returns A QueryBuilder with grouping applied (enables aggregate functions in SELECT and HAVING)\n *\n * @example\n * ```ts\n * // Group by a single column\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .select(({posts, count}) => ({\n * userId: posts.userId,\n * postCount: count()\n * }))\n *\n * // Group by multiple columns\n * query\n * .from({ sales: salesCollection })\n * .groupBy(({sales}) => [sales.region, sales.category])\n * .select(({sales, sum}) => ({\n * region: sales.region,\n * category: sales.category,\n * totalSales: sum(sales.amount)\n * }))\n * ```\n */\n groupBy(callback: GroupByCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const result = callback(refProxy)\n\n const newExpressions = Array.isArray(result)\n ? result.map((r) => toExpression(r))\n : [toExpression(result)]\n\n // Extend existing groupBy expressions (multiple groupBy calls should accumulate)\n const existingGroupBy = this.query.groupBy || []\n return new BaseQueryBuilder({\n ...this.query,\n groupBy: [...existingGroupBy, ...newExpressions],\n }) as any\n }\n\n /**\n * Limit the number of rows returned by the query\n * `orderBy` is required for `limit`\n *\n * @param count - Maximum number of rows to return\n * @returns A QueryBuilder with the limit applied\n *\n * @example\n * ```ts\n * // Get top 5 posts by likes\n * query\n * .from({ posts: postsCollection })\n * .orderBy(({posts}) => posts.likes, 'desc')\n * .limit(5)\n * ```\n */\n limit(count: number): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n limit: count,\n }) as any\n }\n\n /**\n * Skip a number of rows before returning results\n * `orderBy` is required for `offset`\n *\n * @param count - Number of rows to skip\n * @returns A QueryBuilder with the offset applied\n *\n * @example\n * ```ts\n * // Get second page of results\n * query\n * .from({ posts: postsCollection })\n * .orderBy(({posts}) => posts.createdAt, 'desc')\n * .offset(page * pageSize)\n * .limit(pageSize)\n * ```\n */\n offset(count: number): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n offset: count,\n }) as any\n }\n\n /**\n * Specify that the query should return distinct rows.\n * Deduplicates rows based on the selected columns.\n * @returns A QueryBuilder with distinct enabled\n *\n * @example\n * ```ts\n * // Get countries our users are from\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => users.country)\n * .distinct()\n * ```\n */\n distinct(): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n distinct: true,\n }) as any\n }\n\n /**\n * Specify that the query should return a single result\n * @returns A QueryBuilder that returns the first result\n *\n * @example\n * ```ts\n * // Get the user matching the query\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => eq(users.id, 1))\n * .findOne()\n *```\n */\n findOne(): QueryBuilder<TContext & SingleResult> {\n return new BaseQueryBuilder({\n ...this.query,\n // TODO: enforcing return only one result with also a default orderBy if none is specified\n // limit: 1,\n singleResult: true,\n })\n }\n\n // Helper methods\n private _getCurrentAliases(): Array<string> {\n const aliases: Array<string> = []\n\n // Add the from alias\n if (this.query.from) {\n aliases.push(this.query.from.alias)\n }\n\n // Add join aliases\n if (this.query.join) {\n for (const join of this.query.join) {\n aliases.push(join.from.alias)\n }\n }\n\n return aliases\n }\n\n /**\n * Functional variants of the query builder\n * These are imperative function that are called for ery row.\n * Warning: that these cannot be optimized by the query compiler, and may prevent\n * some type of optimizations being possible.\n * @example\n * ```ts\n * q.fn.select((row) => ({\n * name: row.user.name.toUpperCase(),\n * age: row.user.age + 1,\n * }))\n * ```\n */\n get fn() {\n const builder = this\n return {\n /**\n * Select fields using a function that operates on each row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives a row and returns the selected value\n * @returns A QueryBuilder with functional selection applied\n *\n * @example\n * ```ts\n * // Functional select (not optimized)\n * query\n * .from({ users: usersCollection })\n * .fn.select(row => ({\n * name: row.users.name.toUpperCase(),\n * age: row.users.age + 1,\n * }))\n * ```\n */\n select<TFuncSelectResult>(\n callback: (row: TContext[`schema`]) => TFuncSelectResult\n ): QueryBuilder<WithResult<TContext, TFuncSelectResult>> {\n return new BaseQueryBuilder({\n ...builder.query,\n select: undefined, // remove the select clause if it exists\n fnSelect: callback,\n })\n },\n /**\n * Filter rows using a function that operates on each row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives a row and returns a boolean\n * @returns A QueryBuilder with functional filtering applied\n *\n * @example\n * ```ts\n * // Functional where (not optimized)\n * query\n * .from({ users: usersCollection })\n * .fn.where(row => row.users.name.startsWith('A'))\n * ```\n */\n where(\n callback: (row: TContext[`schema`]) => any\n ): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...builder.query,\n fnWhere: [\n ...(builder.query.fnWhere || []),\n callback as (row: NamespacedRow) => any,\n ],\n })\n },\n /**\n * Filter grouped rows using a function that operates on each aggregated row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives an aggregated row and returns a boolean\n * @returns A QueryBuilder with functional having filter applied\n *\n * @example\n * ```ts\n * // Functional having (not optimized)\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .fn.having(row => row.count > 5)\n * ```\n */\n having(\n callback: (row: TContext[`schema`]) => any\n ): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...builder.query,\n fnHaving: [\n ...(builder.query.fnHaving || []),\n callback as (row: NamespacedRow) => any,\n ],\n })\n },\n }\n }\n\n _getQuery(): QueryIR {\n if (!this.query.from) {\n throw new QueryMustHaveFromClauseError()\n }\n return this.query as QueryIR\n }\n}\n\n// Helper to ensure we have a BasicExpression/Aggregate for a value\nfunction toExpr(value: any): BasicExpression | Aggregate {\n if (value === undefined) return toExpression(null)\n if (\n value instanceof AggregateExpr ||\n value instanceof FuncExpr ||\n value instanceof PropRef ||\n value instanceof ValueExpr\n ) {\n return value as BasicExpression | Aggregate\n }\n return toExpression(value)\n}\n\nfunction isPlainObject(value: any): value is Record<string, any> {\n return (\n value !== null &&\n typeof value === `object` &&\n !isExpressionLike(value) &&\n !value.__refProxy\n )\n}\n\nfunction buildNestedSelect(obj: any): any {\n if (!isPlainObject(obj)) return toExpr(obj)\n const out: Record<string, any> = {}\n for (const [k, v] of Object.entries(obj)) {\n if (typeof k === `string` && k.startsWith(`__SPREAD_SENTINEL__`)) {\n // Preserve sentinel key and its value (value is unimportant at compile time)\n out[k] = v\n continue\n }\n out[k] = buildNestedSelect(v)\n }\n return out\n}\n\n// Internal function to build a query from a callback\n// used by liveQueryCollectionOptions.query\nexport function buildQuery<TContext extends Context>(\n fn: (builder: InitialQueryBuilder) => QueryBuilder<TContext>\n): QueryIR {\n const result = fn(new BaseQueryBuilder())\n return getQueryIR(result)\n}\n\n// Internal function to get the QueryIR from a builder\nexport function getQueryIR(\n builder: BaseQueryBuilder | QueryBuilder<any> | InitialQueryBuilder\n): QueryIR {\n return (builder as unknown as BaseQueryBuilder)._getQuery()\n}\n\n// Type-only exports for the query builder\nexport type InitialQueryBuilder = Pick<BaseQueryBuilder<Context>, `from`>\n\nexport type InitialQueryBuilderConstructor = new () => InitialQueryBuilder\n\nexport type QueryBuilder<TContext extends Context> = Omit<\n BaseQueryBuilder<TContext>,\n `from` | `_getQuery`\n>\n\n// Main query builder class alias with the constructor type modified to hide all\n// but the from method on the initial instance\nexport const Query: InitialQueryBuilderConstructor = BaseQueryBuilder\n\n// Helper type to extract context from a QueryBuilder\nexport type ExtractContext<T> =\n T extends BaseQueryBuilder<infer TContext>\n ? TContext\n : T extends QueryBuilder<infer TContext>\n ? TContext\n : never\n\n// Export the types from types.ts for convenience\nexport type {\n Context,\n Source,\n GetResult,\n RefLeaf as Ref,\n InferResultType,\n} from \"./types.js\"\n"],"names":["OnlyOneSourceAllowedError","CollectionImpl","CollectionRef","SubQueryMustHaveFromClauseError","QueryRef","InvalidSourceError","refProxy","createRefProxy","JoinConditionMustBeEqualityError","toExpression","QueryMustHaveFromClauseError","AggregateExpr","FuncExpr","PropRef","ValueExpr","isExpressionLike"],"mappings":";;;;;;AA6CO,MAAM,iBAAqD;AAAA,EAGhE,YAAY,QAA0B,IAAI;AAF1C,SAAiB,QAA0B,CAAA;AAGzC,SAAK,QAAQ,EAAE,GAAG,MAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,oBACN,QACA,SACoC;AACpC,QAAI,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AACpC,YAAM,IAAIA,OAAAA,0BAA0B,OAAO;AAAA,IAC7C;AAEA,UAAM,QAAQ,OAAO,KAAK,MAAM,EAAE,CAAC;AACnC,UAAM,cAAc,OAAO,KAAK;AAEhC,QAAI;AAEJ,QAAI,uBAAuBC,MAAAA,gBAAgB;AACzC,YAAM,IAAIC,GAAAA,cAAc,aAAa,KAAK;AAAA,IAC5C,WAAW,uBAAuB,kBAAkB;AAClD,YAAM,WAAW,YAAY,UAAA;AAC7B,UAAI,CAAE,SAA8B,MAAM;AACxC,cAAM,IAAIC,OAAAA,gCAAgC,OAAO;AAAA,MACnD;AACA,YAAM,IAAIC,GAAAA,SAAS,UAAU,KAAK;AAAA,IACpC,OAAO;AACL,YAAM,IAAIC,OAAAA,mBAAmB,KAAK;AAAA,IACpC;AAEA,WAAO,CAAC,OAAO,GAAG;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,KACE,QAMC;AACD,UAAM,CAAA,EAAG,IAAI,IAAI,KAAK,oBAAoB,QAAQ,aAAa;AAE/D,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR;AAAA,IAAA,CACD;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6BA,KAIE,QACA,YAGA,OAAkB,QAGlB;AACA,UAAM,CAAC,OAAO,IAAI,IAAI,KAAK,oBAAoB,QAAQ,aAAa;AAGpE,UAAM,iBAAiB,KAAK,mBAAA;AAC5B,UAAM,aAAa,CAAC,GAAG,gBAAgB,KAAK;AAC5C,UAAMC,aAAWC,SAAAA,eAAe,UAAU;AAK1C,UAAM,eAAe,WAAWD,UAAQ;AAIxC,QAAI;AACJ,QAAI;AAEJ,QACE,aAAa,SAAS,UACtB,aAAa,SAAS,QACtB,aAAa,KAAK,WAAW,GAC7B;AACA,aAAO,aAAa,KAAK,CAAC;AAC1B,cAAQ,aAAa,KAAK,CAAC;AAAA,IAC7B,OAAO;AACL,YAAM,IAAIE,OAAAA,iCAAA;AAAA,IACZ;AAEA,UAAM,aAAyB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAGF,UAAM,gBAAgB,KAAK,MAAM,QAAQ,CAAA;AAEzC,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR,MAAM,CAAC,GAAG,eAAe,UAAU;AAAA,IAAA,CACpC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,SACE,QACA,YAKA;AACA,WAAO,KAAK,KAAK,QAAQ,YAAY,MAAM;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,UACE,QACA,YAKA;AACA,WAAO,KAAK,KAAK,QAAQ,YAAY,OAAO;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,UACE,QACA,YAKA;AACA,WAAO,KAAK,KAAK,QAAQ,YAAY,OAAO;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,SACE,QACA,YAKA;AACA,WAAO,KAAK,KAAK,QAAQ,YAAY,MAAM;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8BA,MAAM,UAA2D;AAC/D,UAAM,UAAU,KAAK,mBAAA;AACrB,UAAMF,aAAWC,SAAAA,eAAe,OAAO;AACvC,UAAM,aAAa,SAASD,UAAQ;AAEpC,UAAM,gBAAgB,KAAK,MAAM,SAAS,CAAA;AAE1C,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR,OAAO,CAAC,GAAG,eAAe,UAAU;AAAA,IAAA,CACrC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8BA,OAAO,UAA2D;AAChE,UAAM,UAAU,KAAK,mBAAA;AACrB,UAAMA,aAAWC,SAAAA,eAAe,OAAO;AACvC,UAAM,aAAa,SAASD,UAAQ;AAEpC,UAAM,iBAAiB,KAAK,MAAM,UAAU,CAAA;AAE5C,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR,QAAQ,CAAC,GAAG,gBAAgB,UAAU;AAAA,IAAA,CACvC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoCA,OACE,UACyE;AACzE,UAAM,UAAU,KAAK,mBAAA;AACrB,UAAMA,aAAWC,SAAAA,eAAe,OAAO;AACvC,UAAM,eAAe,SAASD,UAAQ;AACtC,UAAM,SAAS,kBAAkB,YAAY;AAE7C,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR;AAAA,MACA,UAAU;AAAA;AAAA,IAAA,CACX;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4BA,QACE,UACA,UAA6C,OACrB;AACxB,UAAM,UAAU,KAAK,mBAAA;AACrB,UAAMA,aAAWC,SAAAA,eAAe,OAAO;AACvC,UAAM,SAAS,SAASD,UAAQ;AAEhC,UAAM,OACJ,OAAO,YAAY,WACf,EAAE,WAAW,SAAS,OAAO,QAAA,IAC7B;AAAA,MACE,WAAW,QAAQ,aAAa;AAAA,MAChC,OAAO,QAAQ,SAAS;AAAA,MACxB,YAAY,QAAQ;AAAA,MACpB,QACE,QAAQ,eAAe,WAAW,QAAQ,SAAS;AAAA,MACrD,eACE,QAAQ,eAAe,WACnB,QAAQ,gBACR;AAAA,IAAA;AAGd,UAAM,oBAAoB,CAAC,QAAa;AACtC,aAAO;AAAA,QACL,YAAYG,SAAAA,aAAa,GAAG;AAAA,QAC5B,gBAAgB;AAAA,MAAA;AAAA,IAEpB;AAGA,UAAM,iBAAiB,MAAM,QAAQ,MAAM,IACvC,OAAO,IAAI,CAAC,MAAM,kBAAkB,CAAC,CAAC,IACtC,CAAC,kBAAkB,MAAM,CAAC;AAE9B,UAAM,kBAA2B,KAAK,MAAM,WAAW,CAAA;AAEvD,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR,SAAS,CAAC,GAAG,iBAAiB,GAAG,cAAc;AAAA,IAAA,CAChD;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8BA,QAAQ,UAA6D;AACnE,UAAM,UAAU,KAAK,mBAAA;AACrB,UAAMH,aAAWC,SAAAA,eAAe,OAAO;AACvC,UAAM,SAAS,SAASD,UAAQ;AAEhC,UAAM,iBAAiB,MAAM,QAAQ,MAAM,IACvC,OAAO,IAAI,CAAC,MAAMG,SAAAA,aAAa,CAAC,CAAC,IACjC,CAACA,SAAAA,aAAa,MAAM,CAAC;AAGzB,UAAM,kBAAkB,KAAK,MAAM,WAAW,CAAA;AAC9C,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR,SAAS,CAAC,GAAG,iBAAiB,GAAG,cAAc;AAAA,IAAA,CAChD;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,MAAM,OAAuC;AAC3C,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR,OAAO;AAAA,IAAA,CACR;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,OAAO,OAAuC;AAC5C,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR,QAAQ;AAAA,IAAA,CACT;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,WAAmC;AACjC,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA,MACR,UAAU;AAAA,IAAA,CACX;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,UAAiD;AAC/C,WAAO,IAAI,iBAAiB;AAAA,MAC1B,GAAG,KAAK;AAAA;AAAA;AAAA,MAGR,cAAc;AAAA,IAAA,CACf;AAAA,EACH;AAAA;AAAA,EAGQ,qBAAoC;AAC1C,UAAM,UAAyB,CAAA;AAG/B,QAAI,KAAK,MAAM,MAAM;AACnB,cAAQ,KAAK,KAAK,MAAM,KAAK,KAAK;AAAA,IACpC;AAGA,QAAI,KAAK,MAAM,MAAM;AACnB,iBAAW,QAAQ,KAAK,MAAM,MAAM;AAClC,gBAAQ,KAAK,KAAK,KAAK,KAAK;AAAA,MAC9B;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,IAAI,KAAK;AACP,UAAM,UAAU;AAChB,WAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAmBL,OACE,UACuD;AACvD,eAAO,IAAI,iBAAiB;AAAA,UAC1B,GAAG,QAAQ;AAAA,UACX,QAAQ;AAAA;AAAA,UACR,UAAU;AAAA,QAAA,CACX;AAAA,MACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAgBA,MACE,UACwB;AACxB,eAAO,IAAI,iBAAiB;AAAA,UAC1B,GAAG,QAAQ;AAAA,UACX,SAAS;AAAA,YACP,GAAI,QAAQ,MAAM,WAAW,CAAA;AAAA,YAC7B;AAAA,UAAA;AAAA,QACF,CACD;AAAA,MACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAiBA,OACE,UACwB;AACxB,eAAO,IAAI,iBAAiB;AAAA,UAC1B,GAAG,QAAQ;AAAA,UACX,UAAU;AAAA,YACR,GAAI,QAAQ,MAAM,YAAY,CAAA;AAAA,YAC9B;AAAA,UAAA;AAAA,QACF,CACD;AAAA,MACH;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,YAAqB;AACnB,QAAI,CAAC,KAAK,MAAM,MAAM;AACpB,YAAM,IAAIC,OAAAA,6BAAA;AAAA,IACZ;AACA,WAAO,KAAK;AAAA,EACd;AACF;AAGA,SAAS,OAAO,OAAyC;AACvD,MAAI,UAAU,OAAW,QAAOD,SAAAA,aAAa,IAAI;AACjD,MACE,iBAAiBE,GAAAA,aACjB,iBAAiBC,GAAAA,QACjB,iBAAiBC,GAAAA,WACjB,iBAAiBC,UACjB;AACA,WAAO;AAAA,EACT;AACA,SAAOL,SAAAA,aAAa,KAAK;AAC3B;AAEA,SAAS,cAAc,OAA0C;AAC/D,SACE,UAAU,QACV,OAAO,UAAU,YACjB,CAACM,oBAAiB,KAAK,KACvB,CAAC,MAAM;AAEX;AAEA,SAAS,kBAAkB,KAAe;AACxC,MAAI,CAAC,cAAc,GAAG,EAAG,QAAO,OAAO,GAAG;AAC1C,QAAM,MAA2B,CAAA;AACjC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,GAAG;AACxC,QAAI,OAAO,MAAM,YAAY,EAAE,WAAW,qBAAqB,GAAG;AAEhE,UAAI,CAAC,IAAI;AACT;AAAA,IACF;AACA,QAAI,CAAC,IAAI,kBAAkB,CAAC;AAAA,EAC9B;AACA,SAAO;AACT;AAIO,SAAS,WACd,IACS;AACT,QAAM,SAAS,GAAG,IAAI,kBAAkB;AACxC,SAAO,WAAW,MAAM;AAC1B;AAGO,SAAS,WACd,SACS;AACT,SAAQ,QAAwC,UAAA;AAClD;AAcO,MAAM,QAAwC;;;;;"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../../../src/query/builder/index.ts"],"sourcesContent":["import { CollectionImpl } from \"../../collection/index.js\"\nimport {\n Aggregate as AggregateExpr,\n CollectionRef,\n Func as FuncExpr,\n PropRef,\n QueryRef,\n Value as ValueExpr,\n isExpressionLike,\n} from \"../ir.js\"\nimport {\n InvalidSourceError,\n InvalidSourceTypeError,\n JoinConditionMustBeEqualityError,\n OnlyOneSourceAllowedError,\n QueryMustHaveFromClauseError,\n SubQueryMustHaveFromClauseError,\n} from \"../../errors.js\"\nimport { createRefProxy, toExpression } from \"./ref-proxy.js\"\nimport type { NamespacedRow, SingleResult } from \"../../types.js\"\nimport type {\n Aggregate,\n BasicExpression,\n JoinClause,\n OrderBy,\n OrderByDirection,\n QueryIR,\n} from \"../ir.js\"\nimport type {\n CompareOptions,\n Context,\n GroupByCallback,\n JoinOnCallback,\n MergeContextForJoinCallback,\n MergeContextWithJoinType,\n OrderByCallback,\n OrderByOptions,\n RefsForContext,\n ResultTypeFromSelect,\n SchemaFromSource,\n SelectObject,\n Source,\n WhereCallback,\n WithResult,\n} from \"./types.js\"\n\nexport class BaseQueryBuilder<TContext extends Context = Context> {\n private readonly query: Partial<QueryIR> = {}\n\n constructor(query: Partial<QueryIR> = {}) {\n this.query = { ...query }\n }\n\n /**\n * Creates a CollectionRef or QueryRef from a source object\n * @param source - An object with a single key-value pair\n * @param context - Context string for error messages (e.g., \"from clause\", \"join clause\")\n * @returns A tuple of [alias, ref] where alias is the source key and ref is the created reference\n */\n private _createRefForSource<TSource extends Source>(\n source: TSource,\n context: string\n ): [string, CollectionRef | QueryRef] {\n // Validate source is a plain object (not null, array, string, etc.)\n // We use try-catch to handle null/undefined gracefully\n let keys: Array<string>\n try {\n keys = Object.keys(source)\n } catch {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const type = source === null ? `null` : `undefined`\n throw new InvalidSourceTypeError(context, type)\n }\n\n // Check if it's an array (arrays pass Object.keys but aren't valid sources)\n if (Array.isArray(source)) {\n throw new InvalidSourceTypeError(context, `array`)\n }\n\n // Validate exactly one key\n if (keys.length !== 1) {\n if (keys.length === 0) {\n throw new InvalidSourceTypeError(context, `empty object`)\n }\n // Check if it looks like a string was passed (has numeric keys)\n if (keys.every((k) => !isNaN(Number(k)))) {\n throw new InvalidSourceTypeError(context, `string`)\n }\n throw new OnlyOneSourceAllowedError(context)\n }\n\n const alias = keys[0]!\n const sourceValue = source[alias]\n\n // Validate the value is a Collection or QueryBuilder\n let ref: CollectionRef | QueryRef\n\n if (sourceValue instanceof CollectionImpl) {\n ref = new CollectionRef(sourceValue, alias)\n } else if (sourceValue instanceof BaseQueryBuilder) {\n const subQuery = sourceValue._getQuery()\n if (!(subQuery as Partial<QueryIR>).from) {\n throw new SubQueryMustHaveFromClauseError(context)\n }\n ref = new QueryRef(subQuery, alias)\n } else {\n throw new InvalidSourceError(alias)\n }\n\n return [alias, ref]\n }\n\n /**\n * Specify the source table or subquery for the query\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @returns A QueryBuilder with the specified source\n *\n * @example\n * ```ts\n * // Query from a collection\n * query.from({ users: usersCollection })\n *\n * // Query from a subquery\n * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)\n * query.from({ activeUsers })\n * ```\n */\n from<TSource extends Source>(\n source: TSource\n ): QueryBuilder<{\n baseSchema: SchemaFromSource<TSource>\n schema: SchemaFromSource<TSource>\n fromSourceName: keyof TSource & string\n hasJoins: false\n }> {\n const [, from] = this._createRefForSource(source, `from clause`)\n\n return new BaseQueryBuilder({\n ...this.query,\n from,\n }) as any\n }\n\n /**\n * Join another table or subquery to the current query\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @param type - The type of join: 'inner', 'left', 'right', or 'full' (defaults to 'left')\n * @returns A QueryBuilder with the joined table available\n *\n * @example\n * ```ts\n * // Left join users with posts\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n *\n * // Inner join with explicit type\n * query\n * .from({ u: usersCollection })\n * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId), 'inner')\n * ```\n *\n * // Join with a subquery\n * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)\n * query\n * .from({ activeUsers })\n * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId))\n */\n join<\n TSource extends Source,\n TJoinType extends `inner` | `left` | `right` | `full` = `left`,\n >(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >,\n type: TJoinType = `left` as TJoinType\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, TJoinType>\n > {\n const [alias, from] = this._createRefForSource(source, `join clause`)\n\n // Create a temporary context for the callback\n const currentAliases = this._getCurrentAliases()\n const newAliases = [...currentAliases, alias]\n const refProxy = createRefProxy(newAliases) as RefsForContext<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n\n // Get the join condition expression\n const onExpression = onCallback(refProxy)\n\n // Extract left and right from the expression\n // For now, we'll assume it's an eq function with two arguments\n let left: BasicExpression\n let right: BasicExpression\n\n if (\n onExpression.type === `func` &&\n onExpression.name === `eq` &&\n onExpression.args.length === 2\n ) {\n left = onExpression.args[0]!\n right = onExpression.args[1]!\n } else {\n throw new JoinConditionMustBeEqualityError()\n }\n\n const joinClause: JoinClause = {\n from,\n type,\n left,\n right,\n }\n\n const existingJoins = this.query.join || []\n\n return new BaseQueryBuilder({\n ...this.query,\n join: [...existingJoins, joinClause],\n }) as any\n }\n\n /**\n * Perform a LEFT JOIN with another table or subquery\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @returns A QueryBuilder with the left joined table available\n *\n * @example\n * ```ts\n * // Left join users with posts\n * query\n * .from({ users: usersCollection })\n * .leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n * ```\n */\n leftJoin<TSource extends Source>(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `left`>\n > {\n return this.join(source, onCallback, `left`)\n }\n\n /**\n * Perform a RIGHT JOIN with another table or subquery\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @returns A QueryBuilder with the right joined table available\n *\n * @example\n * ```ts\n * // Right join users with posts\n * query\n * .from({ users: usersCollection })\n * .rightJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n * ```\n */\n rightJoin<TSource extends Source>(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `right`>\n > {\n return this.join(source, onCallback, `right`)\n }\n\n /**\n * Perform an INNER JOIN with another table or subquery\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @returns A QueryBuilder with the inner joined table available\n *\n * @example\n * ```ts\n * // Inner join users with posts\n * query\n * .from({ users: usersCollection })\n * .innerJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n * ```\n */\n innerJoin<TSource extends Source>(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `inner`>\n > {\n return this.join(source, onCallback, `inner`)\n }\n\n /**\n * Perform a FULL JOIN with another table or subquery\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @returns A QueryBuilder with the full joined table available\n *\n * @example\n * ```ts\n * // Full join users with posts\n * query\n * .from({ users: usersCollection })\n * .fullJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n * ```\n */\n fullJoin<TSource extends Source>(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContextForJoinCallback<TContext, SchemaFromSource<TSource>>\n >\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `full`>\n > {\n return this.join(source, onCallback, `full`)\n }\n\n /**\n * Filter rows based on a condition\n *\n * @param callback - A function that receives table references and returns an expression\n * @returns A QueryBuilder with the where condition applied\n *\n * @example\n * ```ts\n * // Simple condition\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => gt(users.age, 18))\n *\n * // Multiple conditions\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => and(\n * gt(users.age, 18),\n * eq(users.active, true)\n * ))\n *\n * // Multiple where calls are ANDed together\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => gt(users.age, 18))\n * .where(({users}) => eq(users.active, true))\n * ```\n */\n where(callback: WhereCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const expression = callback(refProxy)\n\n const existingWhere = this.query.where || []\n\n return new BaseQueryBuilder({\n ...this.query,\n where: [...existingWhere, expression],\n }) as any\n }\n\n /**\n * Filter grouped rows based on aggregate conditions\n *\n * @param callback - A function that receives table references and returns an expression\n * @returns A QueryBuilder with the having condition applied\n *\n * @example\n * ```ts\n * // Filter groups by count\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .having(({posts}) => gt(count(posts.id), 5))\n *\n * // Filter by average\n * query\n * .from({ orders: ordersCollection })\n * .groupBy(({orders}) => orders.customerId)\n * .having(({orders}) => gt(avg(orders.total), 100))\n *\n * // Multiple having calls are ANDed together\n * query\n * .from({ orders: ordersCollection })\n * .groupBy(({orders}) => orders.customerId)\n * .having(({orders}) => gt(count(orders.id), 5))\n * .having(({orders}) => gt(avg(orders.total), 100))\n * ```\n */\n having(callback: WhereCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const expression = callback(refProxy)\n\n const existingHaving = this.query.having || []\n\n return new BaseQueryBuilder({\n ...this.query,\n having: [...existingHaving, expression],\n }) as any\n }\n\n /**\n * Select specific columns or computed values from the query\n *\n * @param callback - A function that receives table references and returns an object with selected fields or expressions\n * @returns A QueryBuilder that returns only the selected fields\n *\n * @example\n * ```ts\n * // Select specific columns\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => ({\n * name: users.name,\n * email: users.email\n * }))\n *\n * // Select with computed values\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => ({\n * fullName: concat(users.firstName, ' ', users.lastName),\n * ageInMonths: mul(users.age, 12)\n * }))\n *\n * // Select with aggregates (requires GROUP BY)\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .select(({posts, count}) => ({\n * userId: posts.userId,\n * postCount: count(posts.id)\n * }))\n * ```\n */\n select<TSelectObject extends SelectObject>(\n callback: (refs: RefsForContext<TContext>) => TSelectObject\n ): QueryBuilder<WithResult<TContext, ResultTypeFromSelect<TSelectObject>>> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const selectObject = callback(refProxy)\n const select = buildNestedSelect(selectObject)\n\n return new BaseQueryBuilder({\n ...this.query,\n select: select,\n fnSelect: undefined, // remove the fnSelect clause if it exists\n }) as any\n }\n\n /**\n * Sort the query results by one or more columns\n *\n * @param callback - A function that receives table references and returns the field to sort by\n * @param direction - Sort direction: 'asc' for ascending, 'desc' for descending (defaults to 'asc')\n * @returns A QueryBuilder with the ordering applied\n *\n * @example\n * ```ts\n * // Sort by a single column\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.name)\n *\n * // Sort descending\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.createdAt, 'desc')\n *\n * // Multiple sorts (chain orderBy calls)\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.lastName)\n * .orderBy(({users}) => users.firstName)\n * ```\n */\n orderBy(\n callback: OrderByCallback<TContext>,\n options: OrderByDirection | OrderByOptions = `asc`\n ): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const result = callback(refProxy)\n\n const opts: CompareOptions =\n typeof options === `string`\n ? { direction: options, nulls: `first` }\n : {\n direction: options.direction ?? `asc`,\n nulls: options.nulls ?? `first`,\n stringSort: options.stringSort,\n locale:\n options.stringSort === `locale` ? options.locale : undefined,\n localeOptions:\n options.stringSort === `locale`\n ? options.localeOptions\n : undefined,\n }\n\n const makeOrderByClause = (res: any) => {\n return {\n expression: toExpression(res),\n compareOptions: opts,\n }\n }\n\n // Create the new OrderBy structure with expression and direction\n const orderByClauses = Array.isArray(result)\n ? result.map((r) => makeOrderByClause(r))\n : [makeOrderByClause(result)]\n\n const existingOrderBy: OrderBy = this.query.orderBy || []\n\n return new BaseQueryBuilder({\n ...this.query,\n orderBy: [...existingOrderBy, ...orderByClauses],\n }) as any\n }\n\n /**\n * Group rows by one or more columns for aggregation\n *\n * @param callback - A function that receives table references and returns the field(s) to group by\n * @returns A QueryBuilder with grouping applied (enables aggregate functions in SELECT and HAVING)\n *\n * @example\n * ```ts\n * // Group by a single column\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .select(({posts, count}) => ({\n * userId: posts.userId,\n * postCount: count()\n * }))\n *\n * // Group by multiple columns\n * query\n * .from({ sales: salesCollection })\n * .groupBy(({sales}) => [sales.region, sales.category])\n * .select(({sales, sum}) => ({\n * region: sales.region,\n * category: sales.category,\n * totalSales: sum(sales.amount)\n * }))\n * ```\n */\n groupBy(callback: GroupByCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefsForContext<TContext>\n const result = callback(refProxy)\n\n const newExpressions = Array.isArray(result)\n ? result.map((r) => toExpression(r))\n : [toExpression(result)]\n\n // Extend existing groupBy expressions (multiple groupBy calls should accumulate)\n const existingGroupBy = this.query.groupBy || []\n return new BaseQueryBuilder({\n ...this.query,\n groupBy: [...existingGroupBy, ...newExpressions],\n }) as any\n }\n\n /**\n * Limit the number of rows returned by the query\n * `orderBy` is required for `limit`\n *\n * @param count - Maximum number of rows to return\n * @returns A QueryBuilder with the limit applied\n *\n * @example\n * ```ts\n * // Get top 5 posts by likes\n * query\n * .from({ posts: postsCollection })\n * .orderBy(({posts}) => posts.likes, 'desc')\n * .limit(5)\n * ```\n */\n limit(count: number): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n limit: count,\n }) as any\n }\n\n /**\n * Skip a number of rows before returning results\n * `orderBy` is required for `offset`\n *\n * @param count - Number of rows to skip\n * @returns A QueryBuilder with the offset applied\n *\n * @example\n * ```ts\n * // Get second page of results\n * query\n * .from({ posts: postsCollection })\n * .orderBy(({posts}) => posts.createdAt, 'desc')\n * .offset(page * pageSize)\n * .limit(pageSize)\n * ```\n */\n offset(count: number): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n offset: count,\n }) as any\n }\n\n /**\n * Specify that the query should return distinct rows.\n * Deduplicates rows based on the selected columns.\n * @returns A QueryBuilder with distinct enabled\n *\n * @example\n * ```ts\n * // Get countries our users are from\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => users.country)\n * .distinct()\n * ```\n */\n distinct(): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n distinct: true,\n }) as any\n }\n\n /**\n * Specify that the query should return a single result\n * @returns A QueryBuilder that returns the first result\n *\n * @example\n * ```ts\n * // Get the user matching the query\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => eq(users.id, 1))\n * .findOne()\n *```\n */\n findOne(): QueryBuilder<TContext & SingleResult> {\n return new BaseQueryBuilder({\n ...this.query,\n // TODO: enforcing return only one result with also a default orderBy if none is specified\n // limit: 1,\n singleResult: true,\n })\n }\n\n // Helper methods\n private _getCurrentAliases(): Array<string> {\n const aliases: Array<string> = []\n\n // Add the from alias\n if (this.query.from) {\n aliases.push(this.query.from.alias)\n }\n\n // Add join aliases\n if (this.query.join) {\n for (const join of this.query.join) {\n aliases.push(join.from.alias)\n }\n }\n\n return aliases\n }\n\n /**\n * Functional variants of the query builder\n * These are imperative function that are called for ery row.\n * Warning: that these cannot be optimized by the query compiler, and may prevent\n * some type of optimizations being possible.\n * @example\n * ```ts\n * q.fn.select((row) => ({\n * name: row.user.name.toUpperCase(),\n * age: row.user.age + 1,\n * }))\n * ```\n */\n get fn() {\n const builder = this\n return {\n /**\n * Select fields using a function that operates on each row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives a row and returns the selected value\n * @returns A QueryBuilder with functional selection applied\n *\n * @example\n * ```ts\n * // Functional select (not optimized)\n * query\n * .from({ users: usersCollection })\n * .fn.select(row => ({\n * name: row.users.name.toUpperCase(),\n * age: row.users.age + 1,\n * }))\n * ```\n */\n select<TFuncSelectResult>(\n callback: (row: TContext[`schema`]) => TFuncSelectResult\n ): QueryBuilder<WithResult<TContext, TFuncSelectResult>> {\n return new BaseQueryBuilder({\n ...builder.query,\n select: undefined, // remove the select clause if it exists\n fnSelect: callback,\n })\n },\n /**\n * Filter rows using a function that operates on each row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives a row and returns a boolean\n * @returns A QueryBuilder with functional filtering applied\n *\n * @example\n * ```ts\n * // Functional where (not optimized)\n * query\n * .from({ users: usersCollection })\n * .fn.where(row => row.users.name.startsWith('A'))\n * ```\n */\n where(\n callback: (row: TContext[`schema`]) => any\n ): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...builder.query,\n fnWhere: [\n ...(builder.query.fnWhere || []),\n callback as (row: NamespacedRow) => any,\n ],\n })\n },\n /**\n * Filter grouped rows using a function that operates on each aggregated row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives an aggregated row and returns a boolean\n * @returns A QueryBuilder with functional having filter applied\n *\n * @example\n * ```ts\n * // Functional having (not optimized)\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .fn.having(row => row.count > 5)\n * ```\n */\n having(\n callback: (row: TContext[`schema`]) => any\n ): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...builder.query,\n fnHaving: [\n ...(builder.query.fnHaving || []),\n callback as (row: NamespacedRow) => any,\n ],\n })\n },\n }\n }\n\n _getQuery(): QueryIR {\n if (!this.query.from) {\n throw new QueryMustHaveFromClauseError()\n }\n return this.query as QueryIR\n }\n}\n\n// Helper to ensure we have a BasicExpression/Aggregate for a value\nfunction toExpr(value: any): BasicExpression | Aggregate {\n if (value === undefined) return toExpression(null)\n if (\n value instanceof AggregateExpr ||\n value instanceof FuncExpr ||\n value instanceof PropRef ||\n value instanceof ValueExpr\n ) {\n return value as BasicExpression | Aggregate\n }\n return toExpression(value)\n}\n\nfunction isPlainObject(value: any): value is Record<string, any> {\n return (\n value !== null &&\n typeof value === `object` &&\n !isExpressionLike(value) &&\n !value.__refProxy\n )\n}\n\nfunction buildNestedSelect(obj: any): any {\n if (!isPlainObject(obj)) return toExpr(obj)\n const out: Record<string, any> = {}\n for (const [k, v] of Object.entries(obj)) {\n if (typeof k === `string` && k.startsWith(`__SPREAD_SENTINEL__`)) {\n // Preserve sentinel key and its value (value is unimportant at compile time)\n out[k] = v\n continue\n }\n out[k] = buildNestedSelect(v)\n }\n return out\n}\n\n// Internal function to build a query from a callback\n// used by liveQueryCollectionOptions.query\nexport function buildQuery<TContext extends Context>(\n fn: (builder: InitialQueryBuilder) => QueryBuilder<TContext>\n): QueryIR {\n const result = fn(new BaseQueryBuilder())\n return getQueryIR(result)\n}\n\n// Internal function to get the QueryIR from a builder\nexport function getQueryIR(\n builder: BaseQueryBuilder | QueryBuilder<any> | InitialQueryBuilder\n): QueryIR {\n return (builder as unknown as BaseQueryBuilder)._getQuery()\n}\n\n// Type-only exports for the query builder\nexport type InitialQueryBuilder = Pick<BaseQueryBuilder<Context>, `from`>\n\nexport type InitialQueryBuilderConstructor = new () => InitialQueryBuilder\n\nexport type QueryBuilder<TContext extends Context> = Omit<\n BaseQueryBuilder<TContext>,\n `from` | `_getQuery`\n>\n\n// Main query builder class alias with the constructor type modified to hide all\n// but the from method on the initial instance\nexport const Query: InitialQueryBuilderConstructor = BaseQueryBuilder\n\n// Helper type to extract context from a QueryBuilder\nexport type ExtractContext<T> =\n T extends BaseQueryBuilder<infer TContext>\n ? TContext\n : T extends QueryBuilder<infer TContext>\n ? TContext\n : never\n\n// Export the types from types.ts for convenience\nexport type {\n Context,\n Source,\n GetResult,\n RefLeaf as Ref,\n InferResultType,\n} from \"./types.js\"\n"],"names":["InvalidSourceTypeError","OnlyOneSourceAllowedError","CollectionImpl","CollectionRef","SubQueryMustHaveFromClauseError","QueryRef","InvalidSourceError","refProxy","createRefProxy","JoinConditionMustBeEqualityError","toExpression","QueryMustHaveFromClauseError","AggregateExpr","FuncExpr","PropRef","ValueExpr","isExpressionLike"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
3
|
+
const liteDebouncer = require("@tanstack/pacer-lite/lite-debouncer");
|
|
4
4
|
function debounceStrategy(options) {
|
|
5
|
-
const debouncer
|
|
5
|
+
const debouncer = new liteDebouncer.LiteDebouncer(
|
|
6
6
|
(callback) => callback(),
|
|
7
7
|
options
|
|
8
8
|
);
|
|
@@ -10,10 +10,10 @@ function debounceStrategy(options) {
|
|
|
10
10
|
_type: `debounce`,
|
|
11
11
|
options,
|
|
12
12
|
execute: (fn) => {
|
|
13
|
-
debouncer
|
|
13
|
+
debouncer.maybeExecute(fn);
|
|
14
14
|
},
|
|
15
15
|
cleanup: () => {
|
|
16
|
-
debouncer
|
|
16
|
+
debouncer.cancel();
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debounceStrategy.cjs","sources":["../../../src/strategies/debounceStrategy.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"debounceStrategy.cjs","sources":["../../../src/strategies/debounceStrategy.ts"],"sourcesContent":["import { LiteDebouncer } from \"@tanstack/pacer-lite/lite-debouncer\"\nimport type { DebounceStrategy, DebounceStrategyOptions } from \"./types\"\nimport type { Transaction } from \"../transactions\"\n\n/**\n * Creates a debounce strategy that delays transaction execution until after\n * a period of inactivity.\n *\n * Ideal for scenarios like search inputs or auto-save fields where you want\n * to wait for the user to stop typing before persisting changes.\n *\n * @param options - Configuration for the debounce behavior\n * @returns A debounce strategy instance\n *\n * @example\n * ```ts\n * const mutate = usePacedMutations({\n * onMutate: (value) => {\n * collection.update(id, draft => { draft.value = value })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 500 })\n * })\n * ```\n */\nexport function debounceStrategy(\n options: DebounceStrategyOptions\n): DebounceStrategy {\n const debouncer = new LiteDebouncer(\n (callback: () => Transaction) => callback(),\n options\n )\n\n return {\n _type: `debounce`,\n options,\n execute: <T extends object = Record<string, unknown>>(\n fn: () => Transaction<T>\n ) => {\n debouncer.maybeExecute(fn as () => Transaction)\n },\n cleanup: () => {\n debouncer.cancel()\n },\n }\n}\n"],"names":["LiteDebouncer"],"mappings":";;;AA2BO,SAAS,iBACd,SACkB;AAClB,QAAM,YAAY,IAAIA,cAAAA;AAAAA,IACpB,CAAC,aAAgC,SAAA;AAAA,IACjC;AAAA,EAAA;AAGF,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA,SAAS,CACP,OACG;AACH,gBAAU,aAAa,EAAuB;AAAA,IAChD;AAAA,IACA,SAAS,MAAM;AACb,gBAAU,OAAA;AAAA,IACZ;AAAA,EAAA;AAEJ;;"}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
3
|
+
const liteQueuer = require("@tanstack/pacer-lite/lite-queuer");
|
|
4
4
|
function queueStrategy(options) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
let processingChain = Promise.resolve();
|
|
6
|
+
const queuer = new liteQueuer.LiteQueuer(
|
|
7
|
+
(fn) => {
|
|
8
|
+
processingChain = processingChain.then(async () => {
|
|
9
|
+
const transaction = fn();
|
|
10
|
+
await transaction.isPersisted.promise;
|
|
11
|
+
}).catch(() => {
|
|
12
|
+
});
|
|
9
13
|
},
|
|
10
14
|
{
|
|
11
|
-
|
|
12
|
-
// Process one at a time to ensure serialization
|
|
13
|
-
wait: options?.wait,
|
|
15
|
+
wait: options?.wait ?? 0,
|
|
14
16
|
maxSize: options?.maxSize,
|
|
15
17
|
addItemsTo: options?.addItemsTo ?? `back`,
|
|
16
18
|
// Default FIFO: add to back
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queueStrategy.cjs","sources":["../../../src/strategies/queueStrategy.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"queueStrategy.cjs","sources":["../../../src/strategies/queueStrategy.ts"],"sourcesContent":["import { LiteQueuer } from \"@tanstack/pacer-lite/lite-queuer\"\nimport type { QueueStrategy, QueueStrategyOptions } from \"./types\"\nimport type { Transaction } from \"../transactions\"\n\n/**\n * Creates a queue strategy that processes all mutations in order with proper serialization.\n *\n * Unlike other strategies that may drop executions, queue ensures every\n * mutation is processed sequentially. Each transaction commit completes before\n * the next one starts. Useful when data consistency is critical and\n * every operation must complete in order.\n *\n * @param options - Configuration for queue behavior (FIFO/LIFO, timing, size limits)\n * @returns A queue strategy instance\n *\n * @example\n * ```ts\n * // FIFO queue - process in order received\n * const mutate = usePacedMutations({\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: queueStrategy({\n * wait: 200,\n * addItemsTo: 'back',\n * getItemsFrom: 'front'\n * })\n * })\n * ```\n *\n * @example\n * ```ts\n * // LIFO queue - process most recent first\n * const mutate = usePacedMutations({\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: queueStrategy({\n * wait: 200,\n * addItemsTo: 'back',\n * getItemsFrom: 'back'\n * })\n * })\n * ```\n */\nexport function queueStrategy(options?: QueueStrategyOptions): QueueStrategy {\n // Manual promise chaining to ensure async serialization\n // LiteQueuer (unlike AsyncQueuer from @tanstack/pacer) lacks built-in async queue\n // primitives and concurrency control. We compensate by manually chaining promises\n // to ensure each transaction completes before the next one starts.\n let processingChain = Promise.resolve()\n\n const queuer = new LiteQueuer<() => Transaction>(\n (fn) => {\n // Chain each transaction to the previous one's completion\n processingChain = processingChain\n .then(async () => {\n const transaction = fn()\n // Wait for the transaction to be persisted before processing next item\n await transaction.isPersisted.promise\n })\n .catch(() => {\n // Errors are handled via transaction.isPersisted.promise and surfaced there.\n // This catch prevents unhandled promise rejections from breaking the chain,\n // ensuring subsequent transactions can still execute even if one fails.\n })\n },\n {\n wait: options?.wait ?? 0,\n maxSize: options?.maxSize,\n addItemsTo: options?.addItemsTo ?? `back`, // Default FIFO: add to back\n getItemsFrom: options?.getItemsFrom ?? `front`, // Default FIFO: get from front\n started: true, // Start processing immediately\n }\n )\n\n return {\n _type: `queue`,\n options,\n execute: <T extends object = Record<string, unknown>>(\n fn: () => Transaction<T>\n ) => {\n // Add the transaction-creating function to the queue\n queuer.addItem(fn as () => Transaction)\n },\n cleanup: () => {\n queuer.stop()\n queuer.clear()\n },\n }\n}\n"],"names":["LiteQueuer"],"mappings":";;;AA6CO,SAAS,cAAc,SAA+C;AAK3E,MAAI,kBAAkB,QAAQ,QAAA;AAE9B,QAAM,SAAS,IAAIA,WAAAA;AAAAA,IACjB,CAAC,OAAO;AAEN,wBAAkB,gBACf,KAAK,YAAY;AAChB,cAAM,cAAc,GAAA;AAEpB,cAAM,YAAY,YAAY;AAAA,MAChC,CAAC,EACA,MAAM,MAAM;AAAA,MAIb,CAAC;AAAA,IACL;AAAA,IACA;AAAA,MACE,MAAM,SAAS,QAAQ;AAAA,MACvB,SAAS,SAAS;AAAA,MAClB,YAAY,SAAS,cAAc;AAAA;AAAA,MACnC,cAAc,SAAS,gBAAgB;AAAA;AAAA,MACvC,SAAS;AAAA;AAAA,IAAA;AAAA,EACX;AAGF,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA,SAAS,CACP,OACG;AAEH,aAAO,QAAQ,EAAuB;AAAA,IACxC;AAAA,IACA,SAAS,MAAM;AACb,aAAO,KAAA;AACP,aAAO,MAAA;AAAA,IACT;AAAA,EAAA;AAEJ;;"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
3
|
+
const liteThrottler = require("@tanstack/pacer-lite/lite-throttler");
|
|
4
4
|
function throttleStrategy(options) {
|
|
5
|
-
const throttler
|
|
5
|
+
const throttler = new liteThrottler.LiteThrottler(
|
|
6
6
|
(callback) => callback(),
|
|
7
7
|
options
|
|
8
8
|
);
|
|
@@ -10,10 +10,10 @@ function throttleStrategy(options) {
|
|
|
10
10
|
_type: `throttle`,
|
|
11
11
|
options,
|
|
12
12
|
execute: (fn) => {
|
|
13
|
-
throttler
|
|
13
|
+
throttler.maybeExecute(fn);
|
|
14
14
|
},
|
|
15
15
|
cleanup: () => {
|
|
16
|
-
throttler
|
|
16
|
+
throttler.cancel();
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"throttleStrategy.cjs","sources":["../../../src/strategies/throttleStrategy.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"throttleStrategy.cjs","sources":["../../../src/strategies/throttleStrategy.ts"],"sourcesContent":["import { LiteThrottler } from \"@tanstack/pacer-lite/lite-throttler\"\nimport type { ThrottleStrategy, ThrottleStrategyOptions } from \"./types\"\nimport type { Transaction } from \"../transactions\"\n\n/**\n * Creates a throttle strategy that ensures transactions are evenly spaced\n * over time.\n *\n * Provides smooth, controlled execution patterns ideal for UI updates like\n * sliders, progress bars, or scroll handlers where you want consistent\n * execution timing.\n *\n * @param options - Configuration for throttle behavior\n * @returns A throttle strategy instance\n *\n * @example\n * ```ts\n * // Throttle slider updates to every 200ms\n * const mutate = usePacedMutations({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => { draft.value = volume })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n * ```\n *\n * @example\n * ```ts\n * // Throttle with leading and trailing execution\n * const mutate = usePacedMutations({\n * onMutate: (data) => {\n * collection.update(id, draft => { Object.assign(draft, data) })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: throttleStrategy({\n * wait: 500,\n * leading: true,\n * trailing: true\n * })\n * })\n * ```\n */\nexport function throttleStrategy(\n options: ThrottleStrategyOptions\n): ThrottleStrategy {\n const throttler = new LiteThrottler(\n (callback: () => Transaction) => callback(),\n options\n )\n\n return {\n _type: `throttle`,\n options,\n execute: <T extends object = Record<string, unknown>>(\n fn: () => Transaction<T>\n ) => {\n throttler.maybeExecute(fn as () => Transaction)\n },\n cleanup: () => {\n throttler.cancel()\n },\n }\n}\n"],"names":["LiteThrottler"],"mappings":";;;AA+CO,SAAS,iBACd,SACkB;AAClB,QAAM,YAAY,IAAIA,cAAAA;AAAAA,IACpB,CAAC,aAAgC,SAAA;AAAA,IACjC;AAAA,EAAA;AAGF,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA,SAAS,CACP,OACG;AACH,gBAAU,aAAa,EAAuB;AAAA,IAChD;AAAA,IACA,SAAS,MAAM;AACb,gBAAU,OAAA;AAAA,IACZ;AAAA,EAAA;AAEJ;;"}
|
package/dist/cjs/types.d.cts
CHANGED
|
@@ -198,10 +198,12 @@ export type LoadSubsetOptions = {
|
|
|
198
198
|
subscription?: Subscription;
|
|
199
199
|
};
|
|
200
200
|
export type LoadSubsetFn = (options: LoadSubsetOptions) => true | Promise<void>;
|
|
201
|
+
export type UnloadSubsetFn = (options: LoadSubsetOptions) => void;
|
|
201
202
|
export type CleanupFn = () => void;
|
|
202
203
|
export type SyncConfigRes = {
|
|
203
204
|
cleanup?: CleanupFn;
|
|
204
205
|
loadSubset?: LoadSubsetFn;
|
|
206
|
+
unloadSubset?: UnloadSubsetFn;
|
|
205
207
|
};
|
|
206
208
|
export interface SyncConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number> {
|
|
207
209
|
sync: (params: {
|
|
@@ -26,6 +26,11 @@ export declare class CollectionSubscription extends EventEmitter<SubscriptionEve
|
|
|
26
26
|
private options;
|
|
27
27
|
private loadedInitialState;
|
|
28
28
|
private snapshotSent;
|
|
29
|
+
/**
|
|
30
|
+
* Track all loadSubset calls made by this subscription so we can unload them on cleanup.
|
|
31
|
+
* We store the exact LoadSubsetOptions we passed to loadSubset to ensure symmetric unload.
|
|
32
|
+
*/
|
|
33
|
+
private loadedSubsets;
|
|
29
34
|
private sentKeys;
|
|
30
35
|
private filteredCallback;
|
|
31
36
|
private orderByIndex;
|
|
@@ -11,6 +11,7 @@ class CollectionSubscription extends EventEmitter {
|
|
|
11
11
|
this.options = options;
|
|
12
12
|
this.loadedInitialState = false;
|
|
13
13
|
this.snapshotSent = false;
|
|
14
|
+
this.loadedSubsets = [];
|
|
14
15
|
this.sentKeys = /* @__PURE__ */ new Set();
|
|
15
16
|
this._status = `ready`;
|
|
16
17
|
this.pendingLoadSubsetPromises = /* @__PURE__ */ new Set();
|
|
@@ -110,10 +111,12 @@ class CollectionSubscription extends EventEmitter {
|
|
|
110
111
|
} else {
|
|
111
112
|
this.loadedInitialState = true;
|
|
112
113
|
}
|
|
113
|
-
const
|
|
114
|
+
const loadOptions = {
|
|
114
115
|
where: stateOpts.where,
|
|
115
116
|
subscription: this
|
|
116
|
-
}
|
|
117
|
+
};
|
|
118
|
+
const syncResult = this.collection._sync.loadSubset(loadOptions);
|
|
119
|
+
this.loadedSubsets.push(loadOptions);
|
|
117
120
|
const trackLoadSubsetPromise = opts?.trackLoadSubsetPromise ?? true;
|
|
118
121
|
if (trackLoadSubsetPromise) {
|
|
119
122
|
this.trackLoadSubsetPromise(syncResult);
|
|
@@ -208,20 +211,24 @@ class CollectionSubscription extends EventEmitter {
|
|
|
208
211
|
const valueFilter = operator(expression, new Value(minValue));
|
|
209
212
|
whereWithValueFilter = where ? and(where, valueFilter) : valueFilter;
|
|
210
213
|
}
|
|
211
|
-
const
|
|
214
|
+
const loadOptions1 = {
|
|
212
215
|
where: whereWithValueFilter,
|
|
213
216
|
limit,
|
|
214
217
|
orderBy,
|
|
215
218
|
subscription: this
|
|
216
|
-
}
|
|
219
|
+
};
|
|
220
|
+
const syncResult = this.collection._sync.loadSubset(loadOptions1);
|
|
221
|
+
this.loadedSubsets.push(loadOptions1);
|
|
217
222
|
const promises = [];
|
|
218
223
|
if (typeof minValue !== `undefined`) {
|
|
219
224
|
const { expression } = orderBy[0];
|
|
220
225
|
const exactValueFilter = eq(expression, new Value(minValue));
|
|
221
|
-
const
|
|
226
|
+
const loadOptions2 = {
|
|
222
227
|
where: exactValueFilter,
|
|
223
228
|
subscription: this
|
|
224
|
-
}
|
|
229
|
+
};
|
|
230
|
+
const equalValueResult = this.collection._sync.loadSubset(loadOptions2);
|
|
231
|
+
this.loadedSubsets.push(loadOptions2);
|
|
225
232
|
if (equalValueResult instanceof Promise) {
|
|
226
233
|
promises.push(equalValueResult);
|
|
227
234
|
}
|
|
@@ -272,6 +279,10 @@ class CollectionSubscription extends EventEmitter {
|
|
|
272
279
|
}
|
|
273
280
|
}
|
|
274
281
|
unsubscribe() {
|
|
282
|
+
for (const options of this.loadedSubsets) {
|
|
283
|
+
this.collection._sync.unloadSubset(options);
|
|
284
|
+
}
|
|
285
|
+
this.loadedSubsets = [];
|
|
275
286
|
this.emitInner(`unsubscribed`, {
|
|
276
287
|
type: `unsubscribed`,
|
|
277
288
|
subscription: this
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.js","sources":["../../../src/collection/subscription.ts"],"sourcesContent":["import { ensureIndexForExpression } from \"../indexes/auto-index.js\"\nimport { and, eq, gt, lt } from \"../query/builder/functions.js\"\nimport { Value } from \"../query/ir.js\"\nimport { EventEmitter } from \"../event-emitter.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 {\n ChangeMessage,\n Subscription,\n SubscriptionEvents,\n SubscriptionStatus,\n SubscriptionUnsubscribedEvent,\n} from \"../types.js\"\nimport type { CollectionImpl } from \"./index.js\"\n\ntype RequestSnapshotOptions = {\n where?: BasicExpression<boolean>\n optimizedOnly?: boolean\n trackLoadSubsetPromise?: boolean\n}\n\ntype RequestLimitedSnapshotOptions = {\n orderBy: OrderBy\n limit: number\n minValue?: any\n}\n\ntype CollectionSubscriptionOptions = {\n includeInitialState?: boolean\n /** Pre-compiled expression for filtering changes */\n whereExpression?: BasicExpression<boolean>\n /** Callback to call when the subscription is unsubscribed */\n onUnsubscribe?: (event: SubscriptionUnsubscribedEvent) => void\n}\n\nexport class CollectionSubscription\n extends EventEmitter<SubscriptionEvents>\n implements Subscription\n{\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 // Status tracking\n private _status: SubscriptionStatus = `ready`\n private pendingLoadSubsetPromises: Set<Promise<void>> = new Set()\n\n public get status(): SubscriptionStatus {\n return this._status\n }\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 super()\n if (options.onUnsubscribe) {\n this.on(`unsubscribed`, (event) => options.onUnsubscribe!(event))\n }\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 /**\n * Set subscription status and emit events if changed\n */\n private setStatus(newStatus: SubscriptionStatus) {\n if (this._status === newStatus) {\n return // No change\n }\n\n const previousStatus = this._status\n this._status = newStatus\n\n // Emit status:change event\n this.emitInner(`status:change`, {\n type: `status:change`,\n subscription: this,\n previousStatus,\n status: newStatus,\n })\n\n // Emit specific status event\n const eventKey: `status:${SubscriptionStatus}` = `status:${newStatus}`\n this.emitInner(eventKey, {\n type: eventKey,\n subscription: this,\n previousStatus,\n status: newStatus,\n } as SubscriptionEvents[typeof eventKey])\n }\n\n /**\n * Track a loadSubset promise and manage loading status\n */\n private trackLoadSubsetPromise(syncResult: Promise<void> | true) {\n // Track the promise if it's actually a promise (async work)\n if (syncResult instanceof Promise) {\n this.pendingLoadSubsetPromises.add(syncResult)\n this.setStatus(`loadingSubset`)\n\n syncResult.finally(() => {\n this.pendingLoadSubsetPromises.delete(syncResult)\n if (this.pendingLoadSubsetPromises.size === 0) {\n this.setStatus(`ready`)\n }\n })\n }\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 const syncResult = this.collection._sync.loadSubset({\n where: stateOpts.where,\n subscription: this,\n })\n\n const trackLoadSubsetPromise = opts?.trackLoadSubsetPromise ?? true\n if (trackLoadSubsetPromise) {\n this.trackLoadSubsetPromise(syncResult)\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 fulfills the `where` clause and all rows are bigger or equal to `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 1: it may load more rows than the provided LIMIT because it loads all values equal to `minValue` + limit values greater than `minValue`.\n * This is needed to ensure that it does not accidentally skip duplicate values when the limit falls in the middle of some duplicated values.\n * Note 2: 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\n // If we have a minValue we need to handle the case\n // where there might be duplicate values equal to minValue that we need to include\n // because we can have data like this: [1, 2, 3, 3, 3, 4, 5]\n // so if minValue is 3 then the previous snapshot may not have included all 3s\n // e.g. if it was offset 0 and limit 3 it would only have loaded the first 3\n // so we load all rows equal to minValue first, to be sure we don't skip any duplicate values\n let keys: Array<string | number> = []\n if (minValue !== undefined) {\n // First, get all items with the same value as minValue\n const { expression } = orderBy[0]!\n const allRowsWithMinValue = this.collection.currentStateAsChanges({\n where: eq(expression, new Value(minValue)),\n })\n\n if (allRowsWithMinValue) {\n const keysWithMinValue = allRowsWithMinValue\n .map((change) => change.key)\n .filter((key) => !this.sentKeys.has(key) && filterFn(key))\n\n // Add items with the minValue first\n keys.push(...keysWithMinValue)\n\n // Then get items greater than minValue\n const keysGreaterThanMin = index.take(\n limit - keys.length,\n minValue,\n filterFn\n )\n keys.push(...keysGreaterThanMin)\n } else {\n keys = index.take(limit, minValue, filterFn)\n }\n } else {\n keys = index.take(limit, minValue, filterFn)\n }\n\n const valuesNeeded = () => Math.max(limit - changes.length, 0)\n const collectionExhausted = () => keys.length === 0\n\n while (valuesNeeded() > 0 && !collectionExhausted()) {\n const insertedKeys = new Set<string | number>() // Track keys we add to `changes` in this iteration\n\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 insertedKeys.add(key) // Track this key\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 const syncResult = this.collection._sync.loadSubset({\n where: whereWithValueFilter,\n limit,\n orderBy,\n subscription: this,\n })\n\n // Make parallel loadSubset calls for values equal to minValue and values greater than minValue\n const promises: Array<Promise<void>> = []\n\n // First promise: load all values equal to minValue\n if (typeof minValue !== `undefined`) {\n const { expression } = orderBy[0]!\n const exactValueFilter = eq(expression, new Value(minValue))\n\n const equalValueResult = this.collection._sync.loadSubset({\n where: exactValueFilter,\n subscription: this,\n })\n\n if (equalValueResult instanceof Promise) {\n promises.push(equalValueResult)\n }\n }\n\n // Second promise: load values greater than minValue\n if (syncResult instanceof Promise) {\n promises.push(syncResult)\n }\n\n // Track the combined promise\n if (promises.length > 0) {\n const combinedPromise = Promise.all(promises).then(() => {})\n this.trackLoadSubsetPromise(combinedPromise)\n } else {\n this.trackLoadSubsetPromise(syncResult)\n }\n }\n\n // TODO: also add similar test but that checks that it can also load it from the collection's loadSubset function\n // and that that also works properly (i.e. does not skip duplicate values)\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.emitInner(`unsubscribed`, {\n type: `unsubscribed`,\n subscription: this,\n })\n // Clear all event listeners to prevent memory leaks\n this.clearListeners()\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, eq, gt, lt } from \"../query/builder/functions.js\"\nimport { Value } from \"../query/ir.js\"\nimport { EventEmitter } from \"../event-emitter.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 {\n ChangeMessage,\n LoadSubsetOptions,\n Subscription,\n SubscriptionEvents,\n SubscriptionStatus,\n SubscriptionUnsubscribedEvent,\n} from \"../types.js\"\nimport type { CollectionImpl } from \"./index.js\"\n\ntype RequestSnapshotOptions = {\n where?: BasicExpression<boolean>\n optimizedOnly?: boolean\n trackLoadSubsetPromise?: boolean\n}\n\ntype RequestLimitedSnapshotOptions = {\n orderBy: OrderBy\n limit: number\n minValue?: any\n}\n\ntype CollectionSubscriptionOptions = {\n includeInitialState?: boolean\n /** Pre-compiled expression for filtering changes */\n whereExpression?: BasicExpression<boolean>\n /** Callback to call when the subscription is unsubscribed */\n onUnsubscribe?: (event: SubscriptionUnsubscribedEvent) => void\n}\n\nexport class CollectionSubscription\n extends EventEmitter<SubscriptionEvents>\n implements Subscription\n{\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 /**\n * Track all loadSubset calls made by this subscription so we can unload them on cleanup.\n * We store the exact LoadSubsetOptions we passed to loadSubset to ensure symmetric unload.\n */\n private loadedSubsets: Array<LoadSubsetOptions> = []\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 // Status tracking\n private _status: SubscriptionStatus = `ready`\n private pendingLoadSubsetPromises: Set<Promise<void>> = new Set()\n\n public get status(): SubscriptionStatus {\n return this._status\n }\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 super()\n if (options.onUnsubscribe) {\n this.on(`unsubscribed`, (event) => options.onUnsubscribe!(event))\n }\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 /**\n * Set subscription status and emit events if changed\n */\n private setStatus(newStatus: SubscriptionStatus) {\n if (this._status === newStatus) {\n return // No change\n }\n\n const previousStatus = this._status\n this._status = newStatus\n\n // Emit status:change event\n this.emitInner(`status:change`, {\n type: `status:change`,\n subscription: this,\n previousStatus,\n status: newStatus,\n })\n\n // Emit specific status event\n const eventKey: `status:${SubscriptionStatus}` = `status:${newStatus}`\n this.emitInner(eventKey, {\n type: eventKey,\n subscription: this,\n previousStatus,\n status: newStatus,\n } as SubscriptionEvents[typeof eventKey])\n }\n\n /**\n * Track a loadSubset promise and manage loading status\n */\n private trackLoadSubsetPromise(syncResult: Promise<void> | true) {\n // Track the promise if it's actually a promise (async work)\n if (syncResult instanceof Promise) {\n this.pendingLoadSubsetPromises.add(syncResult)\n this.setStatus(`loadingSubset`)\n\n syncResult.finally(() => {\n this.pendingLoadSubsetPromises.delete(syncResult)\n if (this.pendingLoadSubsetPromises.size === 0) {\n this.setStatus(`ready`)\n }\n })\n }\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 const loadOptions: LoadSubsetOptions = {\n where: stateOpts.where,\n subscription: this,\n }\n const syncResult = this.collection._sync.loadSubset(loadOptions)\n\n // Track this loadSubset call so we can unload it later\n this.loadedSubsets.push(loadOptions)\n\n const trackLoadSubsetPromise = opts?.trackLoadSubsetPromise ?? true\n if (trackLoadSubsetPromise) {\n this.trackLoadSubsetPromise(syncResult)\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 fulfills the `where` clause and all rows are bigger or equal to `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 1: it may load more rows than the provided LIMIT because it loads all values equal to `minValue` + limit values greater than `minValue`.\n * This is needed to ensure that it does not accidentally skip duplicate values when the limit falls in the middle of some duplicated values.\n * Note 2: 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\n // If we have a minValue we need to handle the case\n // where there might be duplicate values equal to minValue that we need to include\n // because we can have data like this: [1, 2, 3, 3, 3, 4, 5]\n // so if minValue is 3 then the previous snapshot may not have included all 3s\n // e.g. if it was offset 0 and limit 3 it would only have loaded the first 3\n // so we load all rows equal to minValue first, to be sure we don't skip any duplicate values\n let keys: Array<string | number> = []\n if (minValue !== undefined) {\n // First, get all items with the same value as minValue\n const { expression } = orderBy[0]!\n const allRowsWithMinValue = this.collection.currentStateAsChanges({\n where: eq(expression, new Value(minValue)),\n })\n\n if (allRowsWithMinValue) {\n const keysWithMinValue = allRowsWithMinValue\n .map((change) => change.key)\n .filter((key) => !this.sentKeys.has(key) && filterFn(key))\n\n // Add items with the minValue first\n keys.push(...keysWithMinValue)\n\n // Then get items greater than minValue\n const keysGreaterThanMin = index.take(\n limit - keys.length,\n minValue,\n filterFn\n )\n keys.push(...keysGreaterThanMin)\n } else {\n keys = index.take(limit, minValue, filterFn)\n }\n } else {\n keys = index.take(limit, minValue, filterFn)\n }\n\n const valuesNeeded = () => Math.max(limit - changes.length, 0)\n const collectionExhausted = () => keys.length === 0\n\n while (valuesNeeded() > 0 && !collectionExhausted()) {\n const insertedKeys = new Set<string | number>() // Track keys we add to `changes` in this iteration\n\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 insertedKeys.add(key) // Track this key\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 const loadOptions1: LoadSubsetOptions = {\n where: whereWithValueFilter,\n limit,\n orderBy,\n subscription: this,\n }\n const syncResult = this.collection._sync.loadSubset(loadOptions1)\n\n // Track this loadSubset call\n this.loadedSubsets.push(loadOptions1)\n\n // Make parallel loadSubset calls for values equal to minValue and values greater than minValue\n const promises: Array<Promise<void>> = []\n\n // First promise: load all values equal to minValue\n if (typeof minValue !== `undefined`) {\n const { expression } = orderBy[0]!\n const exactValueFilter = eq(expression, new Value(minValue))\n\n const loadOptions2: LoadSubsetOptions = {\n where: exactValueFilter,\n subscription: this,\n }\n const equalValueResult = this.collection._sync.loadSubset(loadOptions2)\n\n // Track this loadSubset call\n this.loadedSubsets.push(loadOptions2)\n\n if (equalValueResult instanceof Promise) {\n promises.push(equalValueResult)\n }\n }\n\n // Second promise: load values greater than minValue\n if (syncResult instanceof Promise) {\n promises.push(syncResult)\n }\n\n // Track the combined promise\n if (promises.length > 0) {\n const combinedPromise = Promise.all(promises).then(() => {})\n this.trackLoadSubsetPromise(combinedPromise)\n } else {\n this.trackLoadSubsetPromise(syncResult)\n }\n }\n\n // TODO: also add similar test but that checks that it can also load it from the collection's loadSubset function\n // and that that also works properly (i.e. does not skip duplicate values)\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 // Unload all subsets that this subscription loaded\n // We pass the exact same LoadSubsetOptions we used for loadSubset\n for (const options of this.loadedSubsets) {\n this.collection._sync.unloadSubset(options)\n }\n this.loadedSubsets = []\n\n this.emitInner(`unsubscribed`, {\n type: `unsubscribed`,\n subscription: this,\n })\n // Clear all event listeners to prevent memory leaks\n this.clearListeners()\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@ export declare class CollectionSyncManager<TOutput extends object = Record<strin
|
|
|
15
15
|
preloadPromise: Promise<void> | null;
|
|
16
16
|
syncCleanupFn: (() => void) | null;
|
|
17
17
|
syncLoadSubsetFn: ((options: LoadSubsetOptions) => true | Promise<void>) | null;
|
|
18
|
+
syncUnloadSubsetFn: ((options: LoadSubsetOptions) => void) | null;
|
|
18
19
|
private pendingLoadSubsetPromises;
|
|
19
20
|
/**
|
|
20
21
|
* Creates a new CollectionSyncManager instance
|
|
@@ -52,5 +53,10 @@ export declare class CollectionSyncManager<TOutput extends object = Record<strin
|
|
|
52
53
|
* Returns true if no sync function is configured, if syncMode is 'eager', or if there is no work to do.
|
|
53
54
|
*/
|
|
54
55
|
loadSubset(options: LoadSubsetOptions): Promise<void> | true;
|
|
56
|
+
/**
|
|
57
|
+
* Notifies the sync layer that a subset is no longer needed.
|
|
58
|
+
* @param options Options that identify what data is being unloaded
|
|
59
|
+
*/
|
|
60
|
+
unloadSubset(options: LoadSubsetOptions): void;
|
|
55
61
|
cleanup(): void;
|
|
56
62
|
}
|
|
@@ -9,6 +9,7 @@ class CollectionSyncManager {
|
|
|
9
9
|
this.preloadPromise = null;
|
|
10
10
|
this.syncCleanupFn = null;
|
|
11
11
|
this.syncLoadSubsetFn = null;
|
|
12
|
+
this.syncUnloadSubsetFn = null;
|
|
12
13
|
this.pendingLoadSubsetPromises = /* @__PURE__ */ new Set();
|
|
13
14
|
this.config = config;
|
|
14
15
|
this.id = id;
|
|
@@ -112,6 +113,7 @@ class CollectionSyncManager {
|
|
|
112
113
|
);
|
|
113
114
|
this.syncCleanupFn = syncRes?.cleanup ?? null;
|
|
114
115
|
this.syncLoadSubsetFn = syncRes?.loadSubset ?? null;
|
|
116
|
+
this.syncUnloadSubsetFn = syncRes?.unloadSubset ?? null;
|
|
115
117
|
if (this.syncMode === `on-demand` && !this.syncLoadSubsetFn) {
|
|
116
118
|
throw new CollectionConfigurationError(
|
|
117
119
|
`Collection "${this.id}" is configured with syncMode "on-demand" but the sync function did not return a loadSubset handler. Either provide a loadSubset handler or use syncMode "eager".`
|
|
@@ -130,6 +132,11 @@ class CollectionSyncManager {
|
|
|
130
132
|
if (this.preloadPromise) {
|
|
131
133
|
return this.preloadPromise;
|
|
132
134
|
}
|
|
135
|
+
if (this.syncMode === `on-demand`) {
|
|
136
|
+
console.warn(
|
|
137
|
+
`${this.id ? `[${this.id}] ` : ``}Calling .preload() on a collection with syncMode "on-demand" is a no-op. In on-demand mode, data is only loaded when queries request it. Instead, create a live query and call .preload() on that to load the specific data you need. See https://tanstack.com/blog/tanstack-db-0.5-query-driven-sync for more details.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
133
140
|
this.preloadPromise = new Promise((resolve, reject) => {
|
|
134
141
|
if (this.lifecycle.status === `ready`) {
|
|
135
142
|
resolve();
|
|
@@ -208,6 +215,15 @@ class CollectionSyncManager {
|
|
|
208
215
|
}
|
|
209
216
|
return true;
|
|
210
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Notifies the sync layer that a subset is no longer needed.
|
|
220
|
+
* @param options Options that identify what data is being unloaded
|
|
221
|
+
*/
|
|
222
|
+
unloadSubset(options) {
|
|
223
|
+
if (this.syncUnloadSubsetFn) {
|
|
224
|
+
this.syncUnloadSubsetFn(options);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
211
227
|
cleanup() {
|
|
212
228
|
try {
|
|
213
229
|
if (this.syncCleanupFn) {
|