{"version":3,"file":"virtual-props.cjs","sources":["../../src/virtual-props.ts"],"sourcesContent":["/**\n * Virtual Properties for TanStack DB\n *\n * Virtual properties are computed, read-only properties that provide metadata about rows\n * (sync status, source, selection state) without being part of the persisted data model.\n *\n * Virtual properties are prefixed with `$` to distinguish them from user data fields.\n * User schemas should not include `$`-prefixed fields as they are reserved.\n */\n\n/**\n * Collection attribution for a row's current value.\n *\n * - `'local'`: An optimistic row, or a source write attributed through a\n *   same-key local mutation\n * - `'remote'`: A source write without that local attribution\n *\n * Synced Collections infer attribution from key and timing, without a source\n * client ID. With one persisting local mutation and no truncate, the first queued\n * same-key source transaction published at successful mutation settlement\n * consumes local attribution. Its surviving row is `'local'`; later source\n * transactions are `'remote'`. A failed mutation gives those queued\n * writes no local attribution. A truncate can publish while a mutation remains\n * active, leaving its same-key row `'local'` even if the mutation later fails.\n * An unconsumed active mutation on a key omitted by the truncate retains\n * attribution for its first later same-key source transaction, even in that\n * drain. A truncate's write consumes attribution for its key, so a later\n * same-key transaction in that drain is `'remote'` without a new local owner.\n * A source write on a still-pending manual mutation's key applies immediately\n * and can keep `'local'` attribution if that mutation rolls back.\n * When two same-key mutations both persist before a source transaction is\n * queued, with no truncate, a successful one retains one local attribution\n * for the key even if its sibling fails; two failures retain none.\n * An independent peer write can be labeled `'local'`, and a later confirmation\n * from this client can be labeled `'remote'`. Local-only Collections always use\n * `'local'`.\n */\nexport type VirtualOrigin = 'local' | 'remote'\n\n/**\n * Virtual properties recognized on TanStack DB rows.\n *\n * These properties are:\n * - Computed (not stored in the data model)\n * - Read-only (cannot be mutated directly)\n * - Available in queries (WHERE, ORDER BY, SELECT)\n * - Included when spreading rows (`...user`)\n *\n * Structured whole-row selections and spreads retain source metadata.\n * A functional selection retains it only when returning a source row directly.\n * New functional objects and other projections receive the output Collection's\n * metadata. Selected virtual names cannot change that attribution.\n *\n * @template TKey - The type of the row's key (string or number)\n *\n * @example\n * ```typescript\n * // Accessing virtual properties on a row\n * const user = collection.get('user-1')\n * if (!user.$hasPendingWrites) {\n *   console.log('No pending local optimistic writes for this row')\n * }\n * if (user.$origin === 'local') {\n *   console.log('Row has local attribution')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Using virtual properties in queries\n * const ordersWithoutLocalWrites = createLiveQueryCollection({\n *   query: (q) => q\n *     .from({ order: orders })\n *     .where(({ order }) => eq(order.$hasPendingWrites, false))\n * })\n * ```\n */\nexport interface VirtualRowProps<\n  TKey extends string | number = string | number,\n> {\n  /**\n   * Whether this row currently has pending local optimistic writes.\n   *\n   * This describes the row's local optimistic state, not backend upload or\n   * acknowledgement. It is always `false` for local-only collections.\n   */\n  readonly $hasPendingWrites: boolean\n\n  /**\n   * Collection attribution for this row's current value. `'local'` covers\n   * optimistic rows and attributed source writes; `'remote'` covers source\n   * writes without local attribution. See {@link VirtualOrigin} for the\n   * key-and-timing rules, including truncate and overlapping mutations.\n   *\n   * For local-only collections, this is always `'local'`.\n   * Whole-row live queries preserve source attribution. Projected row roots\n   * otherwise use the live-query Collection's attribution.\n   */\n  readonly $origin: VirtualOrigin\n\n  /**\n   * The row's key (primary identifier).\n   *\n   * This is the same value returned by `collection.config.getKey(row)`.\n   * Useful when you need the key in projections or computations.\n   */\n  readonly $key: TKey\n\n  /**\n   * The ID of the source collection this row originated from.\n   *\n   * In joins, this can help identify which collection each row came from.\n   * Whole-row live queries preserve the upstream Collection's ID. Projected\n   * row roots otherwise use the live-query Collection's ID.\n   */\n  readonly $collectionId: string\n}\n\n/**\n * Adds virtual properties to a row type.\n *\n * @template T - The base row type\n * @template TKey - The type of the row's key\n *\n * @example\n * ```typescript\n * type User = { id: string; name: string }\n * type UserWithVirtual = WithVirtualProps<User, string>\n * // { id: string; name: string; $hasPendingWrites: boolean; $origin: 'local' | 'remote'; $key: string; $collectionId: string }\n * ```\n */\nexport type WithVirtualProps<\n  T extends object,\n  TKey extends string | number = string | number,\n> = T & VirtualRowProps<TKey>\n\n/**\n * Extracts the base type from a type that may have virtual properties.\n * Useful when you need to work with the raw data without virtual properties.\n *\n * @template T - The type that may include virtual properties\n *\n * @example\n * ```typescript\n * type UserWithVirtual = { id: string; name: string; $hasPendingWrites: boolean; $origin: 'local' | 'remote' }\n * type User = WithoutVirtualProps<UserWithVirtual>\n * // { id: string; name: string }\n * ```\n */\nexport type WithoutVirtualProps<T> = T extends unknown\n  ? Omit<T, keyof VirtualRowProps>\n  : never\n\n/**\n * Checks if a value has all virtual properties attached.\n *\n * @param value - The value to check\n * @returns true if the value has virtual properties\n *\n * @example\n * ```typescript\n * if (hasVirtualProps(row)) {\n *   console.log('Pending local writes:', row.$hasPendingWrites)\n * }\n * ```\n */\nexport function hasVirtualProps(\n  value: unknown,\n): value is VirtualRowProps<string | number> {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    ['$hasPendingWrites', '$origin', '$key', '$collectionId'].every(\n      (name) => name in value,\n    )\n  )\n}\n\n/**\n * Enriches a row with virtual properties using the \"add-if-missing\" pattern.\n *\n * If the row already has virtual properties (from an upstream collection),\n * they are preserved. If not, new virtual properties are computed and added.\n *\n * This is the key function that enables pass-through semantics for nested\n * live query collections.\n *\n * @param row - The row to enrich\n * @param key - The row's key\n * @param collectionId - The collection's ID\n * @param computePendingWrites - Function to compute $hasPendingWrites if missing\n * @param computeOrigin - Function to compute $origin if missing\n * @returns The row with virtual properties (possibly the same object if already present)\n *\n * @internal\n */\nexport function enrichRowWithVirtualProps<\n  T extends object,\n  TKey extends string | number,\n>(\n  row: T,\n  key: TKey,\n  collectionId: string,\n  computePendingWrites: () => boolean,\n  computeOrigin: () => VirtualOrigin,\n): WithVirtualProps<T, TKey> {\n  // Use nullish coalescing to preserve existing virtual properties (pass-through)\n  // This is the \"add-if-missing\" pattern described in the RFC\n  const existingRow = row as Partial<VirtualRowProps<TKey>>\n  const hasPendingWrites =\n    existingRow.$hasPendingWrites ?? computePendingWrites()\n\n  return {\n    ...row,\n    $hasPendingWrites: hasPendingWrites,\n    $origin: existingRow.$origin ?? computeOrigin(),\n    $key: existingRow.$key ?? key,\n    $collectionId: existingRow.$collectionId ?? collectionId,\n  } as WithVirtualProps<T, TKey>\n}\n\n/**\n * List of virtual property names for iteration and checking.\n * @internal\n */\nexport const VIRTUAL_PROP_NAMES = [\n  '$hasPendingWrites',\n  '$origin',\n  '$key',\n  '$collectionId',\n] as const\n\n/**\n * Checks if a property name is a virtual property.\n * @internal\n */\nexport function isVirtualPropName(name: string): boolean {\n  return VIRTUAL_PROP_NAMES.includes(name as any)\n}\n\n/**\n * Checks whether a property path references a virtual property.\n * @internal\n */\nexport function hasVirtualPropPath(path: Array<string>): boolean {\n  return path.some((segment) => isVirtualPropName(segment))\n}\n"],"names":[],"mappings":";;AAsKO,SAAS,gBACd,OAC2C;AAC3C,SACE,OAAO,UAAU,YACjB,UAAU,QACV,CAAC,qBAAqB,WAAW,QAAQ,eAAe,EAAE;AAAA,IACxD,CAAC,SAAS,QAAQ;AAAA,EAAA;AAGxB;AAoBO,SAAS,0BAId,KACA,KACA,cACA,sBACA,eAC2B;AAG3B,QAAM,cAAc;AACpB,QAAM,mBACJ,YAAY,qBAAqB,qBAAA;AAEnC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,mBAAmB;AAAA,IACnB,SAAS,YAAY,WAAW,cAAA;AAAA,IAChC,MAAM,YAAY,QAAQ;AAAA,IAC1B,eAAe,YAAY,iBAAiB;AAAA,EAAA;AAEhD;AAMO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,SAAS,kBAAkB,MAAuB;AACvD,SAAO,mBAAmB,SAAS,IAAW;AAChD;AAMO,SAAS,mBAAmB,MAA8B;AAC/D,SAAO,KAAK,KAAK,CAAC,YAAY,kBAAkB,OAAO,CAAC;AAC1D;;;;;;"}