@tanstack/db
Version:
A reactive client store for building super fast apps on sync
515 lines (466 loc) • 14.9 kB
text/typescript
import {
compareKeys,
distinct,
filter,
join,
map,
reduce,
serializeValue,
} from '@tanstack/db-ivm'
import { FN_SELECT_STATE, INCLUDES_ROUTING } from '../compiler/index.js'
import { VIRTUAL_PROP_NAMES } from '../../virtual-props.js'
import { deepEquals } from '../../utils.js'
import type {
CompilationResult,
IncludesCompilationResult,
} from '../compiler/index.js'
import type { IncludesMaterialization } from '../ir.js'
import type { IStreamBuilder } from '@tanstack/db-ivm'
import type { ResultStream } from '../../types.js'
type ResultTuple = [
value: Record<PropertyKey, any>,
order: string | undefined,
correlationKey?: unknown,
parentContext?: Record<string, any> | null,
routing?: IncludesRouting,
publicKey?: unknown,
]
type IncludesRouting = Record<string, IncludeRoute>
type IncludeRoute = {
active: boolean
correlationKey: unknown
parentContext: Record<string, any> | null
}
type FnSelectState = {
sourceRow: Record<PropertyKey, any>
fnSelect: (row: any) => unknown
}
type CanonicalResult = {
publicKey: unknown
tuple: ResultTuple
}
export type BucketRow = {
publicKey: unknown
value: Record<PropertyKey, any>
order: string | undefined
}
export type BucketFacadeCompilation = {
edgeId: string
rows: IStreamBuilder<[string, BucketRow]>
activeBuckets: IStreamBuilder<[string, true]>
hasOrderBy: boolean
}
export const BUCKET_FACADE_REF = Symbol(`bucketFacadeRef`)
export type BucketFacadeRef = {
[BUCKET_FACADE_REF]: {
edgeId: string
bucketKey: string
}
}
export type MaterializedCompilation = {
pipeline: ResultStream
facades: Array<BucketFacadeCompilation>
}
type RelationScope = `root` | `child`
type BuiltRelations = WeakMap<
CompilationResult,
Partial<Record<RelationScope, MaterializedCompilation>>
>
let nextBucketFacadeEdgeId = 0
/**
* Compiles inline includes into the same D2 graph as their parent relation.
* Collection-valued includes become inert bucket references. The public facade
* adapter resolves those references after the graph reaches quiescence.
*/
export function materializeCompilation(
compilation: CompilationResult,
getRootKey?: (row: any) => unknown,
reduceJoinedPublicKeys = false,
): MaterializedCompilation {
if (
!compilation.includes?.length &&
!(getRootKey && reduceJoinedPublicKeys)
) {
return { pipeline: compilation.pipeline, facades: [] }
}
const built: BuiltRelations = new WeakMap()
const materialized = materializeRelation(
compilation,
getRootKey,
built,
`root`,
)
return {
...materialized,
facades: dedupeFacades(materialized.facades),
}
}
function materializeRelation(
compilation: CompilationResult,
getKey: ((row: any) => unknown) | undefined,
built: BuiltRelations,
scope: RelationScope,
): MaterializedCompilation {
const cached = built.get(compilation)?.[scope]
if (cached) return cached
let pipeline = canonicalizeByPublicKey(
exposeRouting(compilation.pipeline),
getKey,
scope,
)
const facades: Array<BucketFacadeCompilation> = []
for (const include of compilation.includes ?? []) {
const child = materializeRelation(
include.childCompilationResult,
undefined,
built,
`child`,
)
facades.push(...child.facades)
const bucketRows = createBucketRows(child.pipeline)
if (include.materialization === `collection`) {
const edgeId = `bucket-facade-${++nextBucketFacadeEdgeId}`
const activeBuckets = createActiveBuckets(pipeline, include)
const activeBucketRows = activeBuckets.pipe(
join(bucketRows),
map(([bucketKey, [, row]]) => [bucketKey, row]),
) as IStreamBuilder<[string, BucketRow]>
facades.push({
edgeId,
rows: activeBucketRows,
activeBuckets,
hasOrderBy: include.hasOrderBy,
})
pipeline = attachCollectionInclude(pipeline, include, edgeId, scope)
} else {
pipeline = attachInlineInclude(pipeline, bucketRows, include, scope)
}
}
const result = { pipeline, facades }
built.set(compilation, { ...built.get(compilation), [scope]: result })
return result
}
function dedupeFacades(
facades: Array<BucketFacadeCompilation>,
): Array<BucketFacadeCompilation> {
return [...new Map(facades.map((facade) => [facade.edgeId, facade])).values()]
}
function exposeRouting(pipeline: ResultStream): ResultStream {
return pipeline.pipe(
map(([key, rawTuple]) => {
const tuple = rawTuple as ResultTuple
return [
key,
[
tuple[0],
tuple[1],
tuple[2],
tuple[3],
tuple[0][INCLUDES_ROUTING],
tuple[4],
],
]
}),
) as unknown as ResultStream
}
function canonicalizeByPublicKey(
pipeline: ResultStream,
getKey: ((row: any) => unknown) | undefined,
scope: RelationScope,
): ResultStream {
return pipeline.pipe(
map(([internalKey, rawTuple]) => {
const tuple = rawTuple as ResultTuple
const publicKey = getKey ? getKey(tuple[0]) : (tuple[5] ?? internalKey)
const relationKey =
scope === `root`
? serializeValue([`root`, publicKey])
: serializeValue([routeKey(tuple[2], tuple[3]), publicKey])
return [relationKey, { publicKey, tuple }] as [string, CanonicalResult]
}),
reduce((values: Array<[CanonicalResult, number]>) => {
const totalMultiplicity = values.reduce(
(total, [, multiplicity]) => total + multiplicity,
0,
)
if (totalMultiplicity === 0) return []
if (totalMultiplicity < 0) {
throw new Error(`Canonical query row has negative multiplicity`)
}
const visible = values.find(([, multiplicity]) => multiplicity > 0)?.[0]
if (!visible) {
throw new Error(`Canonical query row has no positive contributor`)
}
for (const [candidate, multiplicity] of values) {
if (multiplicity <= 0) continue
assertCongruentContributors(visible, candidate)
}
return [[visible, 1]]
}),
map(([relationKey, { publicKey, tuple }]) => [
scope === `root` ? publicKey : relationKey,
tuple,
]),
) as ResultStream
}
function assertCongruentContributors(
left: CanonicalResult,
right: CanonicalResult,
): void {
const [leftValue, leftOrder, leftCorrelation, leftContext, leftRouting] =
left.tuple
const [rightValue, rightOrder, rightCorrelation, rightContext, rightRouting] =
right.tuple
if (
leftOrder !== rightOrder ||
!deepEquals(leftValue, rightValue) ||
!deepEquals(leftCorrelation, rightCorrelation) ||
!deepEquals(leftContext, rightContext) ||
!deepEquals(leftRouting, rightRouting)
) {
throw new Error(
`Query contributors for public key ${serializeValue(left.publicKey)} are not congruent`,
)
}
}
function attachInlineInclude(
parentPipeline: ResultStream,
bucketRows: IStreamBuilder<[string, BucketRow]>,
include: IncludesCompilationResult,
scope: RelationScope,
): ResultStream {
const bucketValues = bucketRows.pipe(
reduce((values: Array<[BucketRow, number]>) => {
const rows: Array<BucketRow> = []
for (const [row, multiplicity] of values) {
if (multiplicity < 0) {
throw new Error(
`Materialization bucket row has negative multiplicity`,
)
}
for (let index = 0; index < multiplicity; index++) rows.push(row)
}
if (rows.length === 0) return []
rows.sort(compareBucketRows)
return [[{ value: materializeRows(rows, include) }, 1]]
}),
)
const routedParents = parentPipeline.pipe(
map(([parentKey, rawTuple]) => {
const tuple = rawTuple as ResultTuple
const routing = getIncludeRoute(tuple, include.fieldName)
return [
routing?.active !== true
? `inactive:${serializeValue(parentKey)}`
: routeKey(routing.correlationKey, routing.parentContext),
{ parentKey, tuple },
] as [string, { parentKey: unknown; tuple: ResultTuple }]
}),
join(bucketValues, `left`),
map(([_bucketKey, [parent, bucketValue]]) => {
const [value, order, correlationKey, parentContext, routing, publicKey] =
parent!.tuple
const edgeRouting = routing?.[include.fieldName]
if (edgeRouting?.active !== true) {
return [
parent!.parentKey,
[value, order, correlationKey, parentContext, routing, publicKey],
]
}
const materialized =
bucketValue === null
? emptyMaterializedValue(include.materialization)
: bucketValue.value
return [
parent!.parentKey,
[
setMaterializedInclude(value, include.resultPath, materialized),
order,
correlationKey,
parentContext,
routing,
publicKey,
],
]
}),
)
// A route move can make the join emit matched and empty-bucket deltas for
// the same parent key in one graph turn. Reduce those deltas back to the one
// canonical parent row before the next include or the public output sees it.
return canonicalizeByPublicKey(
routedParents as ResultStream,
undefined,
scope,
)
}
function createBucketRows(
childPipeline: ResultStream,
): IStreamBuilder<[string, BucketRow]> {
return childPipeline.pipe(
map(([internalKey, rawTuple]) => {
const [value, order, correlationKey, parentContext, , publicKey] =
rawTuple as ResultTuple
return [
routeKey(correlationKey, parentContext),
{ publicKey: publicKey ?? internalKey, value, order },
] as [string, BucketRow]
}),
)
}
function attachCollectionInclude(
parentPipeline: ResultStream,
include: IncludesCompilationResult,
edgeId: string,
scope: RelationScope,
): ResultStream {
const routedParents = parentPipeline.pipe(
map(([parentKey, rawTuple]) => {
const tuple = rawTuple as ResultTuple
const routing = getIncludeRoute(tuple, include.fieldName)
if (routing?.active !== true) return [parentKey, tuple]
const facade = createBucketFacadeRef(
edgeId,
routeKey(routing.correlationKey, routing.parentContext),
)
return [
parentKey,
[
setMaterializedInclude(tuple[0], include.resultPath, facade),
tuple[1],
tuple[2],
tuple[3],
tuple[4],
tuple[5],
],
]
}),
)
return canonicalizeByPublicKey(
routedParents as ResultStream,
undefined,
scope,
)
}
function createActiveBuckets(
parentPipeline: ResultStream,
include: IncludesCompilationResult,
): IStreamBuilder<[string, true]> {
return parentPipeline.pipe(
map(([parentKey, rawTuple]) => {
const tuple = rawTuple as ResultTuple
const routing = getIncludeRoute(tuple, include.fieldName)
const bucketKey =
routing?.active === true
? routeKey(routing.correlationKey, routing.parentContext)
: undefined
return [parentKey, bucketKey] as [unknown, string | undefined]
}),
filter(([, bucketKey]) => bucketKey !== undefined),
distinct(([, bucketKey]) => bucketKey),
map(([, bucketKey]) => [bucketKey!, true] as [string, true]),
)
}
function createBucketFacadeRef(
edgeId: string,
bucketKey: string,
): BucketFacadeRef {
return { [BUCKET_FACADE_REF]: { edgeId, bucketKey } }
}
function getIncludeRoute(
tuple: ResultTuple,
fieldName: string,
): IncludeRoute | undefined {
return tuple[4]?.[fieldName]
}
function routeKey(
correlationKey: unknown,
parentContext: Record<string, any> | null | undefined,
): string {
return serializeValue([correlationKey ?? null, parentContext ?? null])
}
function compareBucketRows(left: BucketRow, right: BucketRow): number {
if (left.order !== right.order) {
if (left.order === undefined) return 1
if (right.order === undefined) return -1
return left.order < right.order ? -1 : 1
}
if (
(typeof left.publicKey === `string` ||
typeof left.publicKey === `number`) &&
(typeof right.publicKey === `string` || typeof right.publicKey === `number`)
) {
return compareKeys(left.publicKey, right.publicKey)
}
const leftKey = serializeValue(left.publicKey)
const rightKey = serializeValue(right.publicKey)
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0
}
function materializeRows(
rows: Array<BucketRow>,
include: IncludesCompilationResult,
): unknown {
const scalarField = include.scalarField
const values = scalarField
? rows.map(({ value }) => value[scalarField])
: rows.map(({ value }) => value)
if (include.materialization === `array`) return values
if (include.materialization === `singleton`) return values[0]
return values.map((value) => String(value ?? ``)).join(``)
}
function emptyMaterializedValue(
materialization: IncludesMaterialization,
): unknown {
if (materialization === `array`) return []
if (materialization === `concat`) return ``
if (materialization === `singleton`) return undefined
throw new Error(`Collection includes require a bucket facade`)
}
function setNestedValue(
source: Record<PropertyKey, any>,
path: Array<string>,
value: unknown,
): Record<PropertyKey, any> {
const root = { ...source }
let target = root
let current: Record<PropertyKey, any> | null | undefined = source
for (let index = 0; index < path.length - 1; index++) {
const part = path[index]!
const currentChild: any = current?.[part]
const next = Array.isArray(currentChild)
? [...currentChild]
: { ...(currentChild ?? {}) }
target[part] = next
target = next
current = currentChild
}
target[path[path.length - 1]!] = value
return root
}
function setMaterializedInclude(
value: Record<PropertyKey, any>,
path: Array<string>,
materialized: unknown,
): Record<PropertyKey, any> {
const state = value[FN_SELECT_STATE] as FnSelectState | undefined
if (!state) return setNestedValue(value, path, materialized)
const sourceRow = setNestedValue(state.sourceRow, path, materialized)
const selectedValue = state.fnSelect(sourceRow)
if (!selectedValue || typeof selectedValue !== `object`) {
throw new Error(`fn.select must return an object when it projects includes`)
}
const selected: Record<PropertyKey, any> = Array.isArray(selectedValue)
? [...selectedValue]
: { ...selectedValue }
for (const property of VIRTUAL_PROP_NAMES) {
if (property in value && !(property in selected)) {
selected[property] = value[property]
}
}
selected[INCLUDES_ROUTING] = value[INCLUDES_ROUTING]
Object.defineProperty(selected, FN_SELECT_STATE, {
value: { sourceRow, fnSelect: state.fnSelect },
enumerable: true,
configurable: true,
})
return selected
}