d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
196 lines
6.0 KiB
TypeScript
196 lines
6.0 KiB
TypeScript
import { useEffect, useMemo } from 'react'
|
|
import { createLogger } from '@sim/logger'
|
|
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
|
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
|
|
import { usePersonalEnvironment } from '@/hooks/queries/environment'
|
|
import { getSelectorDefinition, mergeOption } from '@/hooks/selectors/registry'
|
|
import type {
|
|
SelectorKey,
|
|
SelectorOption,
|
|
SelectorPage,
|
|
SelectorQueryArgs,
|
|
} from '@/hooks/selectors/types'
|
|
|
|
interface SelectorHookArgs extends Omit<SelectorQueryArgs, 'key'> {
|
|
search?: string
|
|
detailId?: string
|
|
enabled?: boolean
|
|
}
|
|
|
|
export interface SelectorOptionsResult {
|
|
data: SelectorOption[] | undefined
|
|
isLoading: boolean
|
|
isFetching: boolean
|
|
/**
|
|
* True while paginated selectors are draining remaining pages in the
|
|
* background. Always false for non-paginated selectors.
|
|
*/
|
|
isFetchingMore: boolean
|
|
/**
|
|
* True when the paginated selector still has more pages queued. Always false
|
|
* for non-paginated selectors.
|
|
*/
|
|
hasMore: boolean
|
|
/**
|
|
* True when the paginated drain stopped at {@link MAX_AUTO_DRAIN_PAGES} with
|
|
* pages still remaining, so the option list is a partial view. Always false
|
|
* for non-paginated selectors.
|
|
*/
|
|
truncated: boolean
|
|
error: Error | null
|
|
}
|
|
|
|
const logger = createLogger('SelectorQuery')
|
|
|
|
const EMPTY_PAGE: SelectorPage = { items: [], nextCursor: undefined }
|
|
|
|
/**
|
|
* Safety bound on the background auto-drain. Real dropdowns settle in a handful
|
|
* of pages; this only trips for pathological result sets and prevents an
|
|
* unbounded request loop when a provider keeps handing back cursors.
|
|
*/
|
|
const MAX_AUTO_DRAIN_PAGES = 50
|
|
|
|
export function useSelectorOptions(
|
|
key: SelectorKey,
|
|
args: SelectorHookArgs
|
|
): SelectorOptionsResult {
|
|
const definition = getSelectorDefinition(key)
|
|
const queryArgs: SelectorQueryArgs = {
|
|
key,
|
|
context: args.context,
|
|
search: args.search,
|
|
}
|
|
const isEnabled = args.enabled ?? (definition.enabled ? definition.enabled(queryArgs) : true)
|
|
const supportsPagination = Boolean(definition.fetchPage)
|
|
|
|
const flatQuery = useQuery<SelectorOption[]>({
|
|
queryKey: definition.getQueryKey(queryArgs),
|
|
queryFn: ({ signal }) =>
|
|
definition.fetchList?.({ ...queryArgs, signal }) ?? Promise.resolve([]),
|
|
enabled: !supportsPagination && isEnabled,
|
|
staleTime: definition.staleTime ?? 30_000,
|
|
})
|
|
|
|
const pagedQuery = useInfiniteQuery<SelectorPage>({
|
|
queryKey: [...definition.getQueryKey(queryArgs), 'paged'],
|
|
queryFn: ({ pageParam, signal }) => {
|
|
if (!definition.fetchPage) return Promise.resolve(EMPTY_PAGE)
|
|
return definition.fetchPage({
|
|
...queryArgs,
|
|
cursor: pageParam as string | undefined,
|
|
signal,
|
|
})
|
|
},
|
|
getNextPageParam: (last) => last.nextCursor,
|
|
initialPageParam: undefined as string | undefined,
|
|
enabled: supportsPagination && isEnabled,
|
|
staleTime: definition.staleTime ?? 30_000,
|
|
})
|
|
|
|
const { hasNextPage, isFetchingNextPage, fetchNextPage, isError } = pagedQuery
|
|
const pageCount = pagedQuery.data?.pages.length ?? 0
|
|
const reachedDrainCap = pageCount >= MAX_AUTO_DRAIN_PAGES
|
|
useEffect(() => {
|
|
if (!supportsPagination) return
|
|
if (isError) return
|
|
if (reachedDrainCap) {
|
|
if (hasNextPage) {
|
|
logger.warn('Selector hit auto-drain cap; option list is truncated', {
|
|
key,
|
|
pages: pageCount,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
if (hasNextPage && !isFetchingNextPage) {
|
|
void fetchNextPage()
|
|
}
|
|
}, [
|
|
supportsPagination,
|
|
hasNextPage,
|
|
isFetchingNextPage,
|
|
isError,
|
|
fetchNextPage,
|
|
reachedDrainCap,
|
|
pageCount,
|
|
key,
|
|
])
|
|
|
|
const pagedOptions = useMemo<SelectorOption[] | undefined>(() => {
|
|
if (!supportsPagination) return undefined
|
|
if (!pagedQuery.data) return undefined
|
|
return pagedQuery.data.pages.flatMap((page) => page.items)
|
|
}, [supportsPagination, pagedQuery.data])
|
|
|
|
if (supportsPagination) {
|
|
return {
|
|
data: pagedOptions,
|
|
isLoading: pagedQuery.isLoading,
|
|
isFetching: pagedQuery.isFetching,
|
|
isFetchingMore: pagedQuery.isFetchingNextPage,
|
|
hasMore: (pagedQuery.hasNextPage ?? false) && !reachedDrainCap,
|
|
truncated: reachedDrainCap && (pagedQuery.hasNextPage ?? false),
|
|
error: (pagedQuery.error as Error | null) ?? null,
|
|
}
|
|
}
|
|
|
|
return {
|
|
data: flatQuery.data,
|
|
isLoading: flatQuery.isLoading,
|
|
isFetching: flatQuery.isFetching,
|
|
isFetchingMore: false,
|
|
hasMore: false,
|
|
truncated: false,
|
|
error: (flatQuery.error as Error | null) ?? null,
|
|
}
|
|
}
|
|
|
|
export function useSelectorOptionDetail(
|
|
key: SelectorKey,
|
|
args: SelectorHookArgs & { detailId?: string }
|
|
) {
|
|
const { data: envVariables = {} } = usePersonalEnvironment()
|
|
const definition = getSelectorDefinition(key)
|
|
|
|
const resolvedDetailId = useMemo(() => {
|
|
if (!args.detailId) return undefined
|
|
if (isReference(args.detailId)) return undefined
|
|
if (isEnvVarReference(args.detailId)) {
|
|
const varName = extractEnvVarName(args.detailId)
|
|
return envVariables[varName]?.value || undefined
|
|
}
|
|
return args.detailId
|
|
}, [args.detailId, envVariables])
|
|
|
|
const queryArgs: SelectorQueryArgs = {
|
|
key,
|
|
context: args.context,
|
|
detailId: resolvedDetailId,
|
|
}
|
|
const hasRealDetailId = Boolean(resolvedDetailId)
|
|
const baseEnabled =
|
|
hasRealDetailId && definition.fetchById !== undefined
|
|
? definition.enabled
|
|
? definition.enabled(queryArgs)
|
|
: true
|
|
: false
|
|
const enabled = args.enabled ?? baseEnabled
|
|
|
|
const query = useQuery<SelectorOption | null>({
|
|
queryKey: [...definition.getQueryKey(queryArgs), 'detail', resolvedDetailId ?? 'none'],
|
|
queryFn: ({ signal }) => definition.fetchById!({ ...queryArgs, signal }),
|
|
enabled,
|
|
staleTime: definition.staleTime ?? 300_000,
|
|
})
|
|
|
|
return query
|
|
}
|
|
|
|
export function useSelectorOptionMap(options: SelectorOption[], extra?: SelectorOption | null) {
|
|
return useMemo(() => {
|
|
const merged = mergeOption(options, extra)
|
|
return new Map(merged.map((option) => [option.id, option]))
|
|
}, [options, extra])
|
|
}
|