UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

1 lines 9.55 kB
{"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 * Origin of the last confirmed change to a row, from the current client's perspective.\n *\n * - `'local'`: The change originated from this client (e.g., a mutation made here)\n * - `'remote'`: The change was received via sync from another client/server\n *\n * Note: This reflects the client's perspective, not the original creator.\n * User A creates order → $origin = 'local' on User A's client\n * Order syncs to server\n * User B receives order → $origin = 'remote' on User B's client\n */\nexport type VirtualOrigin = 'local' | 'remote'\n\n/**\n * Virtual properties available on every row in TanStack DB collections.\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 * @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.$synced) {\n * console.log('Confirmed by backend')\n * }\n * if (user.$origin === 'local') {\n * console.log('Created/modified locally')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Using virtual properties in queries\n * const confirmedOrders = createLiveQueryCollection({\n * query: (q) => q\n * .from({ order: orders })\n * .where(({ order }) => eq(order.$synced, true))\n * })\n * ```\n */\nexport interface VirtualRowProps<\n TKey extends string | number = string | number,\n> {\n /**\n * Whether this row reflects confirmed state from the backend.\n *\n * - `true`: Row is confirmed by the backend (no pending optimistic mutations)\n * - `false`: Row has pending optimistic mutations that haven't been confirmed\n *\n * For local-only collections (no sync), this is always `true`.\n * For live query collections, this is passed through from the source collection.\n */\n readonly $synced: boolean\n\n /**\n * Origin of the last confirmed change to this row, from the current client's perspective.\n *\n * - `'local'`: The change originated from this client\n * - `'remote'`: The change was received via sync\n *\n * For local-only collections, this is always `'local'`.\n * For live query collections, this is passed through from the source collection.\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 * For live query collections, this is the ID of the upstream collection.\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; $synced: 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; $synced: boolean; $origin: 'local' | 'remote' }\n * type User = WithoutVirtualProps<UserWithVirtual>\n * // { id: string; name: string }\n * ```\n */\nexport type WithoutVirtualProps<T> = Omit<T, keyof VirtualRowProps>\n\n/**\n * Checks if a value has 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('Synced:', row.$synced)\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 VIRTUAL_PROP_NAMES.every((name) => name in value)\n )\n}\n\n/**\n * Creates virtual properties for a row in a source collection.\n *\n * This is the internal function used by collections to add virtual properties\n * to rows when emitting change messages.\n *\n * @param key - The row's key\n * @param collectionId - The collection's ID\n * @param isSynced - Whether the row is synced (not optimistic)\n * @param origin - Whether the change was local or remote\n * @returns Virtual properties object to merge with the row\n *\n * @internal\n */\nexport function createVirtualProps<TKey extends string | number>(\n key: TKey,\n collectionId: string,\n isSynced: boolean,\n origin: VirtualOrigin,\n): VirtualRowProps<TKey> {\n return {\n $synced: isSynced,\n $origin: origin,\n $key: key,\n $collectionId: collectionId,\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 computeSynced - Function to compute $synced 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 computeSynced: () => 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\n return {\n ...row,\n $synced: existingRow.$synced ?? computeSynced(),\n $origin: existingRow.$origin ?? computeOrigin(),\n $key: existingRow.$key ?? key,\n $collectionId: existingRow.$collectionId ?? collectionId,\n } as WithVirtualProps<T, TKey>\n}\n\n/**\n * Computes aggregate virtual properties for a group of rows.\n *\n * For aggregates:\n * - `$synced`: true if ALL rows in the group are synced; false if ANY row is optimistic\n * - `$origin`: 'local' if ANY row in the group is local; otherwise 'remote'\n *\n * @param rows - The rows in the group\n * @param groupKey - The group key\n * @param collectionId - The collection ID\n * @returns Virtual properties for the aggregate row\n *\n * @internal\n */\nexport function computeAggregateVirtualProps<TKey extends string | number>(\n rows: Array<Partial<VirtualRowProps<string | number>>>,\n groupKey: TKey,\n collectionId: string,\n): VirtualRowProps<TKey> {\n // $synced = true only if ALL rows are synced (false if ANY is optimistic)\n const allSynced = rows.every((row) => row.$synced ?? true)\n\n // $origin = 'local' if ANY row is local (consistent with \"local influence\" semantics)\n const hasLocal = rows.some((row) => row.$origin === 'local')\n\n return {\n $synced: allSynced,\n $origin: hasLocal ? 'local' : 'remote',\n $key: groupKey,\n $collectionId: collectionId,\n }\n}\n\n/**\n * List of virtual property names for iteration and checking.\n * @internal\n */\nexport const VIRTUAL_PROP_NAMES = [\n '$synced',\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":";;AAgJO,SAAS,gBACd,OAC2C;AAC3C,SACE,OAAO,UAAU,YACjB,UAAU,QACV,mBAAmB,MAAM,CAAC,SAAS,QAAQ,KAAK;AAEpD;AAgDO,SAAS,0BAId,KACA,KACA,cACA,eACA,eAC2B;AAG3B,QAAM,cAAc;AAEpB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS,YAAY,WAAW,cAAA;AAAA,IAChC,SAAS,YAAY,WAAW,cAAA;AAAA,IAChC,MAAM,YAAY,QAAQ;AAAA,IAC1B,eAAe,YAAY,iBAAiB;AAAA,EAAA;AAEhD;AAuCO,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;;;;;;"}