@tanstack/db
Version:
A reactive client store for building super fast apps on sync
535 lines (512 loc) • 18.5 kB
text/typescript
import {
buildCursorCurrent,
canExpressCursorOrder,
} from '../../utils/cursor.js'
import { normalizeError } from '../../utils/error.js'
import { runAllCallbacks } from '../../utils/callbacks.js'
import { normalizeOrderByPaths } from '../compiler/expressions.js'
import type {
CollectionSubscription,
ReleaseLoadSubset,
} from '../../collection/subscription.js'
import type {
ChangeMessage,
LoadSubsetOptions,
LoadSubsetRequestResult,
} from '../../types.js'
import type { OrderByOptimizationInfo } from '../compiler/order-by.js'
type OrderedRequestKind = `ordered` | `boundary` | `full-source`
/** Owns the conservative provider-loading policy for one ordered source. */
export class OrderedSourceLoader {
private pending: Promise<unknown> | undefined
// Exact request settlement is not provider extent. This latch only records
// that some request once completed; reset may discard the boundary, and an
// empty page retains it. A failure never reads it before a full-source
// completion sets it again, so it never needs clearing.
private hasSettledSourceRequest = false
private settledSourceBoundary: Record<string, unknown> | undefined
// Independent of finite success: only full-source success repairs ordering.
private needsFullSourceRecovery = false
private requesting = false
// Retaining a demand does not prove it succeeded. Async failure retains it
// (`failed`) for replay; a synchronous startup failure retains nothing.
private fullSource: `none` | `held` | `complete` | `failed` = `none`
// Keep callbacks, not copied requests or rows. Successful full-source work
// subsumes these logical owners; unfinished transports remain observed.
private settledFiniteAcquisitions = new Set<ReleaseLoadSubset>()
// The record's presence blocks automatic retry, including initial requests
// that have no explicit window-operation generation.
private failedRequest:
| { windowOperationGeneration: number | undefined }
| undefined
private failedAcquisitions = new Map<ReleaseLoadSubset, OrderedRequestKind>()
private active = true
private generation = 0
private lastPage: { count: number; boundary: unknown } | undefined
private lastPrefixCount: number | undefined
private lastBoundary: unknown
constructor(
private readonly info: OrderByOptimizationInfo,
private readonly subscription: CollectionSubscription,
private readonly alias: string,
private readonly onResult: (
result: LoadSubsetRequestResult,
holdPublication: boolean,
) => void = () => {},
) {
this.info.isRequesting = () => this.requesting
}
/** Derive invalidation from actual contributions, not a second cursor. */
onSourceChanges(
changes: Array<ChangeMessage<Record<string, unknown>, string | number>>,
sentRows: ReadonlyMap<string | number, Record<string, unknown>> | undefined,
): void {
let hasNewRows = false
for (const change of changes) {
const previous = sentRows?.get(change.key)
if (
change.type !== `insert` &&
previous !== undefined &&
(change.type === `delete` ||
this.info.comparator(previous, change.value) !== 0)
) {
this.invalidateSourceOrdering()
return
}
if (change.type !== `delete` && previous === undefined) hasNewRows = true
}
// New keys, including ties, may need another page. Duplicate delivery or
// an order-equal update cannot invalidate an already attempted request.
if (hasNewRows) this.invalidateCursor()
}
start(): void {
const { index, limit, offset, orderBy, requiresFullSource } = this.info
if (index) this.subscription.setOrderByIndex(index)
if (limit === 0) return
if (requiresFullSource) {
this.loadFullSource()
return
}
if (!index || orderBy.length !== 1) {
this.loadPrefix(offset + limit)
return
}
this.loadPage(offset + limit)
}
loadMore(windowOperationGeneration?: number): Promise<unknown> | undefined {
if (!this.active || this.info.limit === 0 || this.requesting) return
const mayRetryFailure =
this.failedRequest === undefined ||
(windowOperationGeneration !== undefined &&
windowOperationGeneration !==
this.failedRequest.windowOperationGeneration)
if (!mayRetryFailure) return this.pending
if (
(this.failedRequest || this.failedAcquisitions.size > 0) &&
windowOperationGeneration !== undefined
) {
// Move ownership to the explicit replacement before releasing the old
// lease. Adapter cleanup may reenter the loader.
if (this.failedRequest) {
this.failedRequest.windowOperationGeneration = windowOperationGeneration
}
const failedAcquisitions = this.failedAcquisitions
this.failedAcquisitions = new Map()
if (failedAcquisitions.size > 0) {
this.requesting = true
try {
runAllCallbacks(failedAcquisitions.keys())
} finally {
this.requesting = false
}
// Adapter cleanup can synchronously tear down this loader.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!this.active) return
}
}
if (this.fullSource === `failed`) this.fullSource = `none`
else if (this.fullSource !== `none`) return this.pending
if (this.needsFullSourceRecovery || this.info.requiresFullSource) {
this.loadFullSource(windowOperationGeneration)
return this.pending
}
if (!this.info.index || this.info.orderBy.length !== 1) {
this.loadPrefix(
this.info.offset + this.info.limit,
windowOperationGeneration,
)
return this.pending
}
if (!this.info.dataNeeded || this.pending) return this.pending
// A recorded failure always carries recovery debt, so it cannot reach this
// finite path; only the first request needs the whole prefix here.
let count = Math.max(
this.info.dataNeeded(),
this.hasSettledSourceRequest ? 0 : this.info.offset + this.info.limit,
)
if (
windowOperationGeneration !== undefined &&
this.settledSourceBoundary !== undefined
) {
const needed = this.info.offset + this.info.limit
count = Math.max(count, needed - this.countAcquiredRows())
}
if (count > 0) {
this.loadPage(count, windowOperationGeneration)
}
return this.pending
}
loadFullSource(windowOperationGeneration?: number): void {
if (!this.active || this.fullSource !== `none`) return
this.fullSource = `held`
this.requestAndObserve(
(onLoadSubsetResult) => {
this.subscription.requestSnapshot({
trackLoadSubsetPromise: false,
onLoadSubsetResult,
})
},
`full-source`,
windowOperationGeneration,
)
}
private loadPrefix(count: number, windowOperationGeneration?: number): void {
if (!this.active || this.pending) return
if (this.lastPrefixCount === count) {
if ((this.info.dataNeeded?.() ?? 0) > 0) {
this.loadFullSource(windowOperationGeneration)
}
return
}
this.requestAndObserve(
(onLoadSubsetResult) => {
this.subscription.requestSnapshot({
orderBy: normalizeOrderByPaths(this.info.orderBy, this.alias),
limit: count,
trackLoadSubsetPromise: false,
onLoadSubsetResult,
})
},
`ordered`,
windowOperationGeneration,
)
this.lastPrefixCount = count
}
resetCursor(): void {
this.generation++
if (this.fullSource === `complete`) this.fullSource = `held`
this.pending = undefined
this.lastBoundary = undefined
this.settledSourceBoundary = undefined
this.invalidateCursor()
}
settleFullSourceReplay(): void {
// Replay repaired the retained logical acquisition. A later window retry
// must not release that now-successful source demand. A failed finite
// page is still obsolete and must be released by that retry.
if (this.fullSource === `failed`) {
for (const [release, kind] of this.failedAcquisitions) {
if (kind === `full-source`) this.failedAcquisitions.delete(release)
}
this.fullSource = `held`
}
if (this.fullSource !== `none`) {
this.fullSource = `complete`
this.retireSettledFiniteAcquisitions()
}
}
private retireSettledFiniteAcquisitions(): void {
if (
this.fullSource !== `complete` ||
this.subscription.hasPendingTruncateReplacement
)
return
const generation = this.generation
runAllCallbacks(
Array.from(this.settledFiniteAcquisitions, (release) => () => {
if (!this.active || generation !== this.generation) return
this.settledFiniteAcquisitions.delete(release)
release()
}),
)
}
invalidateCursor(): void {
this.lastPage = undefined
this.lastPrefixCount = undefined
}
invalidateSourceOrdering(): void {
this.invalidateCursor()
this.requireFullSourceRecovery()
}
dispose(): void {
this.active = false
this.resetCursor()
this.failedAcquisitions.clear()
this.settledFiniteAcquisitions.clear()
}
private countAcquiredRows(): number {
return this.subscription
.readOrderedSnapshot({
orderBy: normalizeOrderByPaths(this.info.orderBy, this.alias),
limit: this.info.offset + this.info.limit,
})
.filter(
({ value }) =>
this.info.comparator(value, this.settledSourceBoundary) <= 0,
).length
}
private loadPage(count: number, windowOperationGeneration?: number): void {
if (!this.active || this.pending) return
// Rows observed before the first provider request do not prove ordered
// source coverage. In particular, a row inserted while limit is zero must
// not become the cursor when that window first opens.
const startsFromSourcePrefix = this.settledSourceBoundary === undefined
const biggest = this.settledSourceBoundary
let minValues: Array<unknown> | undefined
if (biggest !== undefined) {
const value = this.info.valueExtractorForRawRow(biggest)
if (!canExpressCursorOrder(this.info.orderBy, [value])) {
this.loadPrefix(
this.info.offset + this.info.limit,
windowOperationGeneration,
)
return
}
minValues = [value]
}
const boundary = minValues?.[0]
if (
this.lastPage?.count === count &&
Object.is(this.lastPage.boundary, boundary)
) {
return
}
this.lastPage = { count, boundary }
this.requestAndObserve(
(onLoadSubsetResult) => {
this.subscription.requestLimitedSnapshot({
orderBy: normalizeOrderByPaths(this.info.orderBy, this.alias),
limit: count,
minValues,
// Local rows seen before the first provider request prove neither
// a cursor nor a remote offset. Start the first acquisition at zero.
offset: startsFromSourcePrefix ? 0 : this.countAcquiredRows(),
trackLoadSubsetPromise: false,
onLoadSubsetResult,
})
},
`ordered`,
windowOperationGeneration,
)
}
private observe(
result: LoadSubsetRequestResult,
releaseAcquisition: ReleaseLoadSubset,
kind: OrderedRequestKind,
windowOperationGeneration?: number,
options?: LoadSubsetOptions,
): Promise<void> {
const isFullSource = kind === `full-source`
const generation = this.generation
const complete = (): void => {
if (this.pending === tracked) this.pending = undefined
if (!this.active) return
if (!isFullSource) {
// A replay can replace the physical lease while this older transport
// finishes. Retire its logical owner only outside the replay barrier.
this.settledFiniteAcquisitions.add(releaseAcquisition)
this.retireSettledFiniteAcquisitions()
}
if (generation !== this.generation) return
// A finite request may finish behind an authoritative repair. It cannot
// clear that repair's failure or resume finite refinement around it.
if (!isFullSource && (this.failedRequest || this.fullSource !== `none`))
return
this.failedRequest = undefined
if (kind !== `boundary`) {
this.hasSettledSourceRequest = true
// Source delivery can invalidate the in-flight prefix marker.
if (options?.orderBy && !options.cursor) {
this.lastPrefixCount = options.limit
}
if (!isFullSource && options?.orderBy) {
try {
this.settledSourceBoundary =
this.subscription.readOrderedSnapshot(options).at(-1)?.value ??
this.settledSourceBoundary
} catch (error) {
fail(error)
}
}
}
if (isFullSource) {
this.needsFullSourceRecovery = false
this.fullSource = `complete`
this.retireSettledFiniteAcquisitions()
}
if (kind === `ordered`) {
this.loadBoundary(windowOperationGeneration)
return
}
// A boundary request may add tied rows without filling the query's
// window. Resume forward loading once it settles.
this.loadMore()
}
const settlesAsync = result instanceof Promise
const request = settlesAsync ? result : Promise.resolve()
const fail = (error: unknown) => {
this.settledFiniteAcquisitions.delete(releaseAcquisition)
if (this.pending === tracked) this.pending = undefined
if (!this.active) return
// A failed request may already have written only part of its result.
// None of those rows is a safe continuation boundary.
this.requireFullSourceRecovery()
if (generation !== this.generation) return
// A failed request proves no full-source coverage. An explicit window
// move or later replay may retry it, but an ordinary graph pass must
// not start an eager retry loop.
if (isFullSource) this.fullSource = `failed`
this.recordRequestFailure(windowOperationGeneration)
this.failedAcquisitions.set(releaseAcquisition, kind)
throw error
}
const tracked = request.then(complete, fail)
this.pending = tracked
void tracked.catch(() => {})
// Register each request separately. The operation tracker observes the
// next request before this promise settles, so the logical chain remains
// pending without retaining every ancestor promise until the final page.
this.onResult(
tracked,
settlesAsync && isFullSource && this.needsFullSourceRecovery,
)
return tracked
}
private loadBoundary(
windowOperationGeneration?: number,
): Promise<unknown> | undefined {
const biggest = this.settledSourceBoundary
if (biggest === undefined) return
const value = this.info.valueExtractorForRawRow(biggest)
const orderBy = normalizeOrderByPaths(this.info.orderBy, this.alias)
if (!canExpressCursorOrder(orderBy.slice(0, 1), [value])) {
this.loadFullSource(windowOperationGeneration)
return this.pending
}
// Undefined is not an expressible cursor boundary, so it denotes that no
// tie request has been attempted. Other falsy values remain valid keys.
if (Object.is(this.lastBoundary, value)) {
return this.loadMore()
}
const where = buildCursorCurrent(orderBy, [value])
if (!where) {
this.loadFullSource(windowOperationGeneration)
return this.pending
}
this.lastBoundary = value
return this.requestAndObserve(
(onLoadSubsetResult) => {
this.subscription.requestSnapshot({
where,
trackLoadSubsetPromise: false,
onLoadSubsetResult,
})
},
`boundary`,
windowOperationGeneration,
)
}
private requireFullSourceRecovery(): void {
this.settledSourceBoundary = undefined
this.needsFullSourceRecovery = true
}
private failRequest(
observed:
| {
result: LoadSubsetRequestResult
options: LoadSubsetOptions
release: ReleaseLoadSubset
}
| undefined,
error: Error,
isFullSource: boolean,
windowOperationGeneration?: number,
cancelObservedSettlement = false,
): Error {
if (cancelObservedSettlement) {
this.generation++
this.pending = undefined
}
this.requireFullSourceRecovery()
this.recordRequestFailure(windowOperationGeneration)
if (isFullSource) this.fullSource = `none`
try {
observed?.release({ error })
} catch {
// Cleanup is attempted once and must not replace the request failure.
}
return error
}
/** A failed request blocks ordinary refinement until a new operation. */
private recordRequestFailure(windowOperationGeneration?: number): void {
this.failedRequest = { windowOperationGeneration }
this.invalidateCursor()
this.lastBoundary = undefined
}
/** Observe settlement only after all synchronous request work succeeds. */
private requestAndObserve(
request: (
onResult: (
result: LoadSubsetRequestResult,
options: LoadSubsetOptions,
release: ReleaseLoadSubset,
) => void,
) => void,
kind: OrderedRequestKind,
windowOperationGeneration?: number,
): Promise<void> | undefined {
const isFullSource = kind === `full-source`
let observed:
| {
result: LoadSubsetRequestResult
options: LoadSubsetOptions
release: ReleaseLoadSubset
}
| undefined
this.requesting = true
let observing = false
try {
try {
request((result, options, release) => {
observed = { result, options, release }
})
} finally {
this.requesting = false
}
if (!observed) return
observing = true
return this.observe(
observed.result,
observed.release,
kind,
windowOperationGeneration,
observed.options,
)
} catch (error) {
// Both request and settlement callbacks may reenter through cleanup.
// Keep refinement blocked until failure and release finish unwinding.
this.requesting = true
try {
throw this.failRequest(
observed,
normalizeError(error),
isFullSource,
windowOperationGeneration,
observing,
)
} finally {
this.requesting = false
}
}
}
}