@tanstack/db
Version:
A reactive client store for building super fast apps on sync
175 lines (174 loc) • 6.48 kB
JavaScript
import { inArray } from "../builder/functions.js";
import { PropRef } from "../ir.js";
import { createValueIdentity } from "../equality-value-identity.js";
class SubsetDemandController {
constructor() {
this.states = /* @__PURE__ */ new Map();
this.warnedPlans = /* @__PURE__ */ new Set();
this.valueIdentity = createValueIdentity();
}
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 segment of retired) releaseSegment(subscription, segment);
if (this.states.get(plan.id) !== state) {
return { changed: false, empty: false, ready: true };
}
if (nextKeys.size === 0) {
this.states.delete(plan.id);
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;
if (requestedKeys.size > 0) {
const segment = createSegment(plan, requestedKeys);
if (replace) segment.replaces = [...segments];
segments.push(segment);
const started = startSegment(
subscription,
segment,
() => this.warnUnoptimized(plan)
);
if (this.states.get(plan.id) !== state) {
return { changed: false, empty: false, ready: true };
}
if (!started) throw new Error(`Subset demand snapshot did not start`);
if (replace) {
if (segment.ready instanceof Promise) {
void segment.ready.then(
() => this.finishReplacement(subscription, plan.id, segment),
() => {
}
);
} else {
this.finishReplacement(subscription, plan.id, segment);
}
}
}
const pending = 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
};
}
clear() {
for (const state of this.states.values()) {
for (const segment of state.segments) segment.abortController.abort();
}
this.states.clear();
this.warnedPlans.clear();
this.valueIdentity = createValueIdentity();
}
hasPendingDemand(planId) {
return this.states.get(planId)?.segments.some((segment) => segment.state === `pending`) ?? false;
}
finishReplacement(subscription, planId, replacement) {
const state = this.states.get(planId);
if (!state?.segments.includes(replacement) || !equalKeySets(replacement.keys, state.keys)) {
releaseSegment(subscription, replacement);
return;
}
const replaced = replacement.replaces ?? [];
replacement.replaces = void 0;
state.segments = [replacement];
for (const segment of replaced) releaseSegment(subscription, segment);
}
warnUnoptimized(plan) {
if (this.warnedPlans.has(plan.id)) return;
this.warnedPlans.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) {
let observed = false;
const requested = subscription.requestSnapshot({
where: segment.where,
signal: segment.abortController.signal,
trackLoadSubsetPromise: false,
onUnoptimized,
onLoadSubsetResult: (result) => {
observed = true;
segment.ready = result;
segment.state = result instanceof Promise ? `pending` : `settled`;
}
});
if (segment.ready instanceof Promise) {
void segment.ready.then(
() => {
segment.state = `settled`;
},
() => {
segment.state = `failed`;
}
);
}
return requested && observed;
}
function releaseSegment(subscription, segment) {
segment.abortController.abort();
try {
subscription.releaseSnapshot(segment.where);
} catch {
}
}
export {
SubsetDemandController,
hasPendingJoinedWork
};
//# sourceMappingURL=subset-demand-controller.js.map