@tanstack/db
Version:
A reactive client store for building super fast apps on sync
238 lines (237 loc) • 9.23 kB
JavaScript
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