chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user