UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

109 lines (101 loc) • 3.84 kB
import { getCollectionBuilder } from '../query/live/collection-registry.js' import type { HealthNode } from '../types' import type { CollectionImpl } from './index.js' /** A Collection that another Collection reads, under its query alias. */ export interface HealthSource { readonly alias: string /** Tells apart two sources under one alias, such as two includes. */ readonly id?: string readonly collection: CollectionImpl<any, any, any, any, any> readonly kind?: HealthNode[`kind`] } // One cached node per Collection and source. A node keeps its identity until // its own fields or one of its children change, so a change replaces only the // nodes on its path and readers can compare nodes with `===`. const nodeCache = new WeakMap<object, Map<string, HealthNode>>() /** * Build the health tree for a Collection. A base Collection is one node. A live * query is a node with one child per source alias, in query order, and each * child that is itself a live query recurses. The tree is read from current * state, so every node mirrors its Collection at the moment it is read. */ export function buildHealthTree( collection: CollectionImpl<any, any, any, any, any>, { alias, id, kind }: Omit<HealthSource, `collection`> = { alias: `root`, }, // The reader that caches this node, so a source's node lives only as long // as the query that reads it. owner: object = collection, ): HealthNode { // A live query's tree shape comes from its query definition. const builder = getCollectionBuilder(collection) const sources = builder?.healthSources() ?? [] const { failureReason, error } = collection._lifecycle.problems const children = sources.map((source) => buildHealthTree(source.collection, source, collection), ) return healthNode(owner, id, { alias, collectionId: collection.id, kind: kind ?? (builder ? `live-query` : `source`), status: collection.status, failureReason, error, reloading: collection._lifecycle.reloading, sources: children, }) } /** * A node from its own facts and its children. It has errors when it or * anything below it has a `failureReason` or an `error`. */ export function healthNode( owner: object, id: string | undefined, fields: Omit<HealthNode, `hasErrors`>, ): HealthNode { const { failureReason, error, sources } = fields return cachedNode(owner, id ?? fields.alias, { ...fields, hasErrors: failureReason !== null || error !== null || sources.some((child) => child.hasErrors), }) } /** * Every node in a health tree that shows a problem of its own, in tree order: * a `failureReason` or an `error`. A node whose only problems are below it is * not listed; its descendants are. */ export function findProblems(health: HealthNode): Array<HealthNode> { if (!health.hasErrors) return [] const ownProblem = health.failureReason !== null || health.error !== null return [ ...(ownProblem ? [health] : []), ...health.sources.flatMap(findProblems), ] } /** Return the cached node for this Collection and alias if nothing changed. */ function cachedNode(owner: object, key: string, next: HealthNode): HealthNode { let nodes = nodeCache.get(owner) if (!nodes) nodeCache.set(owner, (nodes = new Map())) const previous = nodes.get(key) if (previous && sameNode(previous, next)) return previous nodes.set(key, next) return next } function sameNode(a: HealthNode, b: HealthNode): boolean { return ( a.collectionId === b.collectionId && a.kind === b.kind && a.status === b.status && a.hasErrors === b.hasErrors && a.failureReason === b.failureReason && a.error === b.error && a.reloading === b.reloading && a.sources.length === b.sources.length && a.sources.every((source, index) => source === b.sources[index]) ) }