UNPKG

@tanstack/react-db

Version:

React integration for @tanstack/db

97 lines (89 loc) • 3.23 kB
import { BaseQueryBuilder, IR, isCollection } from '@tanstack/db' import type { DbClient } from '@tanstack/db' const sourceObjectTokens = new WeakMap<object, number>() let nextSourceObjectToken = 0 export function getSourceObjectToken(source: object): number { let id = sourceObjectTokens.get(source) if (id === undefined) { id = ++nextSourceObjectToken sourceObjectTokens.set(source, id) } return id } export function getPreparedSources( preparedValue: unknown, ): Array<{ id: string }> { if (isCollection(preparedValue)) return [preparedValue] const query = preparedValue instanceof BaseQueryBuilder ? preparedValue : preparedValue && typeof preparedValue === `object` && `query` in preparedValue && preparedValue.query instanceof BaseQueryBuilder ? preparedValue.query : undefined return query ? IR.collectCollectionSources(query._getQuery()).map( ({ collection }) => collection, ) : [] } /** The source object each Collection ID named while one hook is mounted. */ export type SourceIdBindings = { unscoped: Map<string, number> byClient: WeakMap<DbClient, Map<string, number>> } export function createSourceIdBindings(): SourceIdBindings { return { unscoped: new Map(), byClient: new WeakMap() } } /** * Derived query identity names each source by its Collection ID. While a hook * is mounted, an ID must keep naming one source object in each client scope, * or the hook could reuse a live query that reads the old source. Claims the * prepared value's sources, or calls `release` and throws on a collision so a * rejected render keeps no sync-start deferral. */ export function claimSourceIds( bindings: SourceIdBindings, preparedValue: unknown, client: DbClient | undefined, hookName: string, release: () => void, ): void { const prior = client ? bindings.byClient.get(client) : bindings.unscoped const seen = new Map<string, number>() for (const source of getPreparedSources(preparedValue)) { const token = getSourceObjectToken(source) const previous = seen.get(source.id) ?? prior?.get(source.id) if (previous !== undefined && previous !== token) { release() throw new Error( `[${hookName}] Source Collection "${source.id}" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`, ) } seen.set(source.id, token) } const claimed = prior ?? new Map<string, number>() for (const [id, token] of seen) claimed.set(id, token) if (client) bindings.byClient.set(client, claimed) } /** * Resumes every deferred sync start, then rethrows the first startup error, so * one failing source cannot keep a later shared source deferred. */ export function resumeSyncStarts( collections: Set<{ _resumeSyncStart: () => void }>, ): void { const pending = Array.from(collections) collections.clear() const errors: Array<unknown> = [] for (const collection of pending) { try { collection._resumeSyncStart() } catch (error) { errors.push(error) } } if (errors.length) throw errors[0] }