UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

238 lines (237 loc) • 9.23 kB
import { inArray } from "../builder/functions.js"; import { PropRef } from "../ir.js"; import { createValueIdentity } from "../equality-value-identity.js"; import { devBuild, codedMessage } from "../../error-message.js"; import { normalizeError } from "../../utils/error.js"; class SubsetDemandController { /** * `onKeyOutcome`: the latest load of a key the plan needs failed with * `error`; without one, a load of the key succeeded or the plan no longer * needs it. A request error belongs to the key's failed request. */ constructor(onKeyOutcome) { this.fs = onKeyOutcome; this.states = /* @__PURE__ */ new Map(); this.eO = /* @__PURE__ */ new Set(); this.valueIdentity = createValueIdentity(); } /** * A truncate's or a reload's replay was judged: its verdict decides every * request error of the demand it replayed. Its success clears them, and * its failure replaces them with the replay's own failures, which the * query reports for the replay. */ clearRequestErrors() { for (const [planId, state] of this.states) for (const key of state.keys.keys()) this.fs?.(planId, key); } setDemand(subscription, plan, keys) { const nextKeys = canonicalizeKeys(keys, this.valueIdentity); const previous = this.states.get(plan.id); const hasFailedCoverage = previous?.segments.some( (segment) => segment.state === `failed` && intersects(segment.keys, nextKeys) ); if (previous && equalKeySets(previous.keys, nextKeys) && !hasFailedCoverage) { return { changed: false, empty: nextKeys.size === 0, ready: true }; } const segments = (previous?.segments ?? []).filter( (segment) => segment.state !== `failed` && intersects(segment.keys, nextKeys) && (!segment.replaces || equalKeySets(segment.keys, nextKeys)) ); const retired = (previous?.segments ?? []).filter( (segment) => !segments.includes(segment) ); const retryReplacement = previous?.segments.some( (segment) => segment.replaces && segment.state === `failed` && equalKeySets(segment.keys, nextKeys) ); const state = { keys: nextKeys, segments }; this.states.set(plan.id, state); for (const key of previous?.keys.keys() ?? []) if (!nextKeys.has(key)) this.fs?.(plan.id, key); const release = () => releaseSegments(subscription, retired); if (nextKeys.size === 0) { this.states.delete(plan.id); release(); return { changed: true, empty: true, ready: true }; } const coveredKeys = new Set( segments.flatMap((segment) => [...segment.keys.keys()]) ); const added = new Map( [...nextKeys].filter(([key]) => !coveredKeys.has(key)) ); const removed = previous ? [...previous.keys.keys()].some((key) => !nextKeys.has(key)) : false; const replace = retryReplacement || removed && (added.size > 0 || segments.length > 1); const requestedKeys = replace ? nextKeys : added; let replacement; try { if (requestedKeys.size > 0) { const segment = createSegment(plan, requestedKeys); if (replace) { segment.replaces = [...segments]; replacement = segment; } segments.push(segment); const started = startSegment( subscription, segment, () => this.fB(plan), (error) => this.ha(plan.id, segment, error) ); if (this.states.get(plan.id) !== state) { return { changed: false, empty: false, ready: true }; } if (!started) throw new Error( devBuild() && process.env.NODE_ENV !== `production` ? `Subset demand snapshot did not start` : codedMessage(159) ); if (replace) { subscription.replaceSnapshots( segment.where, (segment.replaces ?? []).map((replaced) => replaced.where) ); if (segment.ready instanceof Promise) { void segment.ready.then( () => this.ep(subscription, plan.id, segment), () => { } ); } else { this.ep(subscription, plan.id, segment); } } } } finally { release(); } if (this.states.get(plan.id) !== state) { return { changed: false, empty: false, ready: true }; } const pending = (replacement ? [replacement] : state.segments).filter( (segment) => segment.state === `pending` && intersects(segment.keys, nextKeys) ).map((segment) => segment.ready).filter((ready) => ready instanceof Promise); return { changed: true, empty: false, ready: pending.length > 0 ? Promise.all(pending) : true }; } // The outcome of a segment the plan still holds decides its needed keys: a // success (no `error`) loaded them, and a failure leaves them missing, // unless a later segment loads the key (law 10: a key's latest load // decides it). A released segment's outcome decides nothing. ha(planId, segment, error) { const state = this.states.get(planId); if (!state?.segments.includes(segment)) return; const later = state.segments.slice(state.segments.indexOf(segment) + 1); for (const key of segment.keys.keys()) if (state.keys.has(key) && !(error && later.some((loader) => loader.keys.has(key)))) this.fs?.(planId, key, error); } clear() { for (const state of this.states.values()) { for (const segment of state.segments) segment.abortController.abort(); } this.states.clear(); this.eO.clear(); this.valueIdentity = createValueIdentity(); } hasPendingDemand(planId) { return this.states.get(planId)?.segments.some((segment) => segment.state === `pending`) ?? false; } ep(subscription, planId, replacement) { const state = this.states.get(planId); if (!state?.segments.includes(replacement) || !equalKeySets(replacement.keys, state.keys)) { releaseSegments(subscription, [replacement]); return; } const replaced = replacement.replaces ?? []; replacement.replaces = void 0; state.segments = [replacement]; releaseSegments(subscription, replaced); } fB(plan) { if (!(devBuild() && process.env.NODE_ENV !== `production`)) return; if (this.eO.has(plan.id)) return; this.eO.add(plan.id); const path = plan.path.join(`.`); console.warn( `[TanStack DB]${plan.collectionId ? ` [${plan.collectionId}]` : ``} Join requires an index on "${path}" for efficient loading. Falling back to scanning local data. Consider creating an index on the collection with collection.createIndex((row) => row.${path}) or enable auto-indexing with autoIndex: 'eager' and a defaultIndexType.` ); } } function hasPendingJoinedWork(joinedSourceId, lazySourcesCallbacks, subscriptions, isPendingPlan) { return (lazySourcesCallbacks[joinedSourceId]?.plans?.some( (plan) => isPendingPlan(plan.id) ) ?? false) || subscriptions[joinedSourceId]?.status === `loadingSubset`; } function canonicalizeKeys(keys, valueIdentity) { return new Map( [...keys].map((key) => [valueIdentity.serializeEquality(key), key]) ); } function equalKeySets(left, right) { return left.size === right.size && [...left.keys()].every((key) => right.has(key)); } function intersects(left, right) { return [...left.keys()].some((key) => right.has(key)); } function createSegment(plan, keys) { const where = inArray(new PropRef(plan.path), [...keys.values()]); return { keys, where, abortController: new AbortController(), ready: true, state: `starting` }; } function startSegment(subscription, segment, onUnoptimized, onOutcome) { let observed = false; const requested = subscription.requestSnapshot({ where: segment.where, signal: segment.abortController.signal, trackLoadSubsetPromise: false, onUnoptimized, onLoadSubsetResult: (result, options) => { observed = true; if (!(result instanceof Promise)) { segment.state = `settled`; return; } segment.state = `pending`; const settle = (failure) => { if (options.signal?.aborted) return replayed(subscription).then(() => { segment.state = `settled`; }); segment.state = failure ? `failed` : `settled`; if (subscription.pendingTruncateReplacement) return void 0; const error = failure && normalizeError(failure.error); onOutcome(error); if (error) throw error; return void 0; }; segment.ready = result.then( () => settle(), (error) => settle({ error }) ); } }); return requested && observed; } function replayed(subscription) { const replay = subscription.pendingTruncateReplacement; if (!replay) return Promise.resolve(); const next = () => replayed(subscription); return replay.then(next, next); } function releaseSegments(subscription, segments) { if (segments.length === 0) return; for (const segment of segments) segment.abortController.abort(); subscription.releaseSnapshot(...segments.map((segment) => segment.where)); } export { SubsetDemandController, hasPendingJoinedWork }; //# sourceMappingURL=subset-demand-controller.js.map