|
5 | 5 | */ |
6 | 6 |
|
7 | 7 | import { ApifyClient } from '../apify_client.js'; |
8 | | -import { ACTOR_PRICING_MODEL } from '../const.js'; |
| 8 | +import { ACTOR_PRICING_MODEL, INPUT_SCHEMA_FETCH_CONCURRENCY } from '../const.js'; |
9 | 9 | import type { PaymentProvider } from '../payments/types.js'; |
10 | | -import type { ActorStoreList } from '../types.js'; |
| 10 | +import { actorDefinitionPrunedCache, inputFieldsCache } from '../state.js'; |
| 11 | +import type { ActorInputField, ActorInputSchema, ActorStoreList } from '../types.js'; |
| 12 | +import { runWithConcurrency } from './generic.js'; |
| 13 | +import { logHttpError } from './logging.js'; |
11 | 14 |
|
12 | 15 | /** |
13 | 16 | * Used in search Actors tool to search above the input supplied limit, |
@@ -85,3 +88,93 @@ export function filterRentalActors( |
85 | 88 | || userRentedActorIds.includes(actor.id), |
86 | 89 | ); |
87 | 90 | } |
| 91 | + |
| 92 | +/** |
| 93 | + * Extract minimal field info (name, type, required) from an ActorInputSchema. |
| 94 | + */ |
| 95 | +export function extractInputFields(input: ActorInputSchema): ActorInputField[] { |
| 96 | + const requiredSet = new Set(input.required ?? []); |
| 97 | + return Object.entries(input.properties ?? {}).map(([name, prop]) => ({ |
| 98 | + name, |
| 99 | + type: prop.type ?? 'unknown', |
| 100 | + required: requiredSet.has(name), |
| 101 | + })); |
| 102 | +} |
| 103 | + |
| 104 | +/** |
| 105 | + * Look up cached input fields for an actor by fullName. |
| 106 | + * Checks actorDefinitionPrunedCache for a cached definition with input schema. |
| 107 | + * Returns null on cache miss. |
| 108 | + */ |
| 109 | +function getCachedInputFields(fullName: string): ActorInputField[] | null { |
| 110 | + // Check dedicated input fields cache first |
| 111 | + const cached = inputFieldsCache.get(fullName); |
| 112 | + if (cached) return cached; |
| 113 | + |
| 114 | + // Fall back to full actor definition cache (populated by fetch-actor-details / call-actor) |
| 115 | + const cachedDef = actorDefinitionPrunedCache.get(fullName); |
| 116 | + if (cachedDef?.definition?.input) { |
| 117 | + const fields = extractInputFields(cachedDef.definition.input); |
| 118 | + inputFieldsCache.set(fullName, fields); |
| 119 | + return fields; |
| 120 | + } |
| 121 | + return null; |
| 122 | +} |
| 123 | + |
| 124 | +/** |
| 125 | + * Fetch input schema fields for a single actor from the API. |
| 126 | + * Returns null on failure. |
| 127 | + */ |
| 128 | +async function fetchInputFieldsForActor( |
| 129 | + fullName: string, |
| 130 | + apifyClient: ApifyClient, |
| 131 | +): Promise<ActorInputField[] | null> { |
| 132 | + try { |
| 133 | + const buildClient = await apifyClient.actor(fullName).defaultBuild(); |
| 134 | + const build = await buildClient.get(); |
| 135 | + if (build?.actorDefinition?.input) { |
| 136 | + const input = build.actorDefinition.input as ActorInputSchema; |
| 137 | + const fields = extractInputFields(input); |
| 138 | + inputFieldsCache.set(fullName, fields); |
| 139 | + return fields; |
| 140 | + } |
| 141 | + return null; |
| 142 | + } catch (error) { |
| 143 | + logHttpError(error, `Failed to fetch input schema for '${fullName}'`, { actorName: fullName }); |
| 144 | + return null; |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +/** |
| 149 | + * Fetch input schemas for a list of actors with bounded concurrency. |
| 150 | + * Checks caches for hits, fetches misses from API. |
| 151 | + * Returns a Map<actorFullName, ActorInputField[]>. |
| 152 | + */ |
| 153 | +export async function fetchInputFieldsForActors( |
| 154 | + actors: ActorStoreList[], |
| 155 | + apifyClient: ApifyClient, |
| 156 | +): Promise<Map<string, ActorInputField[]>> { |
| 157 | + const result = new Map<string, ActorInputField[]>(); |
| 158 | + |
| 159 | + // Resolve cache hits first |
| 160 | + const toFetch: { fullName: string }[] = []; |
| 161 | + for (const actor of actors) { |
| 162 | + const fullName = `${actor.username}/${actor.name}`; |
| 163 | + const cached = getCachedInputFields(fullName); |
| 164 | + if (cached) { |
| 165 | + result.set(fullName, cached); |
| 166 | + } else { |
| 167 | + toFetch.push({ fullName }); |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + // Fetch cache misses with bounded concurrency |
| 172 | + await runWithConcurrency(toFetch, INPUT_SCHEMA_FETCH_CONCURRENCY, async ({ fullName }) => { |
| 173 | + const fields = await fetchInputFieldsForActor(fullName, apifyClient); |
| 174 | + if (fields) { |
| 175 | + result.set(fullName, fields); |
| 176 | + } |
| 177 | + }); |
| 178 | + |
| 179 | + return result; |
| 180 | +} |
0 commit comments