@@ -21,23 +21,12 @@ import {
2121import { ACTOR_LIMITS } from '@apify/consts' ;
2222
2323import type { LruCache } from '../../datastructures/src/lru_cache' ;
24+ import type { ActorRunOptions , MemoryEvaluationContext } from './types.js' ;
2425
25- type ActorRunOptions = {
26- build ?: string ;
27- timeoutSecs ?: number ;
28- memoryMbytes ?: number ; // probably no one will need it, but let's keep it consistent
29- diskMbytes ?: number ; // probably no one will need it, but let's keep it consistent
30- maxItems ?: number ;
31- maxTotalChargeUsd ?: number ;
32- restartOnError ?: boolean ;
33- }
34-
35- type MemoryEvaluationContext = {
36- runOptions : ActorRunOptions ;
37- input : Record < string , unknown > ;
38- }
39-
40- export const DEFAULT_MEMORY_MBYTES_MAX_CHARS = 1000 ;
26+ // In theory, users could create expressions longer than 1000 characters,
27+ // but in practice, it's unlikely anyone would need that much complexity.
28+ // Later we can increase this limit if needed.
29+ export const DEFAULT_MEMORY_MBYTES_EXPRESSION_MAX_LENGTH = 1000 ;
4130
4231/**
4332 * A Set of allowed keys from ActorRunOptions that can be used in
@@ -58,14 +47,18 @@ const ALLOWED_RUN_OPTION_KEYS = new Set<keyof ActorRunOptions>([
5847 * MathJS security recommendations: https://mathjs.org/docs/expressions/security.html
5948 */
6049const math = create ( {
50+ // expression dependencies
51+ // Required for compiling and evaluating root expressions.
52+ // We disable it below to prevent users from calling `evaluate()` inside their expressions.
53+ // For example: defaultMemoryMbytes = "evaluate('2 + 2')"
54+ compileDependencies,
55+ evaluateDependencies,
56+
6157 // arithmetic dependencies
6258 addDependencies,
6359 subtractDependencies,
6460 multiplyDependencies,
6561 divideDependencies,
66- // expression dependencies
67- compileDependencies,
68- evaluateDependencies,
6962 // statistics dependencies
7063 maxDependencies,
7164 minDependencies,
@@ -77,8 +70,7 @@ const math = create({
7770 // without that dependency 'null ?? 5', won't work
7871 nullishDependencies,
7972} ) ;
80- const limitedEvaluate = math . evaluate ;
81- const limitedCompile = math . compile ;
73+ const { compile } = math ;
8274
8375// Disable potentially dangerous functions
8476math . import ( {
@@ -88,6 +80,8 @@ math.import({
8880 reviver ( ) { throw new Error ( 'Function reviver is disabled' ) ; } ,
8981
9082 // extra (has functional impact)
83+ // We disable evaluate to prevent users from calling it inside their expressions.
84+ // For example: defaultMemoryMbytes = "evaluate('2 + 2')"
9185 evaluate ( ) { throw new Error ( 'Function evaluate is disabled' ) ; } ,
9286 parse ( ) { throw new Error ( 'Function parse is disabled' ) ; } ,
9387 simplify ( ) { throw new Error ( 'Function simplify is disabled' ) ; } ,
@@ -118,7 +112,7 @@ const customGetFunc = (obj: any, path: string, defaultVal?: number) => {
118112*/
119113const roundToClosestPowerOf2 = ( num : number ) : number | undefined => {
120114 if ( typeof num !== 'number' || Number . isNaN ( num ) ) {
121- throw new Error ( `Failed to round number to a power of 2 .` ) ;
115+ throw new Error ( `Calculated memory value is not a valid number: ${ num } .` ) ;
122116 }
123117
124118 // Handle 0 or negative values.
@@ -135,8 +129,14 @@ const roundToClosestPowerOf2 = (num: number): number | undefined => {
135129} ;
136130
137131/**
138- * Replaces `{{variable}}` placeholders in an expression string with the variable name.
139- * Enforces strict validation to allow `{{input.*}}` paths or whitelisted `{{runOptions.*}}` keys.
132+ * Replaces all `{{variable}}` placeholders in an expression into direct
133+ * property access (e.g. `{{runOptions.memoryMbytes}}` → `runOptions.memoryMbytes`).
134+ *
135+ * Only variables starting with `input.` or whitelisted `runOptions.` keys are allowed.
136+ * All `input.*` values are accepted, while `runOptions.*` are validated (only 7 variables - ALLOWED_RUN_OPTION_KEYS).
137+ *
138+ * Note: this approach allows developers to use a consistent double-brace
139+ * syntax (`{{runOptions.timeoutSecs}}`) across the platform.
140140 *
141141 * @example
142142 * // Returns "runOptions.memoryMbytes + 1024"
@@ -145,26 +145,25 @@ const roundToClosestPowerOf2 = (num: number): number | undefined => {
145145 * @param defaultMemoryMbytes The raw string expression, e.g., "{{runOptions.memoryMbytes}} * 2".
146146 * @returns A safe, processed expression for evaluation, e.g., "runOptions.memoryMbytes * 2".
147147 */
148- const preprocessRunMemoryExpression = ( defaultMemoryMbytes : string ) : string => {
148+ const processTemplateVariables = ( defaultMemoryMbytes : string ) : string => {
149149 const variableRegex = / { { \s * ( [ a - z A - Z 0 - 9 _ . ] + ) \s * } } / g;
150150
151151 const processedExpression = defaultMemoryMbytes . replace (
152152 variableRegex ,
153153 ( _ , variableName : string ) => {
154- // 1. Validate that the variable starts with either 'input.' or 'runOptions.'
155154 if ( ! variableName . startsWith ( 'runOptions.' ) && ! variableName . startsWith ( 'input.' ) ) {
156155 throw new Error (
157156 `Invalid variable '{{${ variableName } }}' in expression. Variables must start with 'input.' or 'runOptions.'.` ,
158157 ) ;
159158 }
160159
161- // 2 . Check if the variable is accessing input (e.g. {{input.someValue}})
162- // We do not validate the specific property name because input is dynamic.
160+ // 1 . Check if the variable is accessing input (e.g. {{input.someValue}})
161+ // We do not validate the specific property name because ` input` is dynamic.
163162 if ( variableName . startsWith ( 'input.' ) ) {
164163 return variableName ;
165164 }
166165
167- // 3 . Check if the variable is accessing runOptions (e.g. {{runOptions.memoryMbytes}}) and validate the keys.
166+ // 2 . Check if the variable is accessing runOptions (e.g. {{runOptions.memoryMbytes}}) and validate the keys.
168167 if ( variableName . startsWith ( 'runOptions.' ) ) {
169168 const key = variableName . slice ( 'runOptions.' . length ) ;
170169 if ( ! ALLOWED_RUN_OPTION_KEYS . has ( key as keyof ActorRunOptions ) ) {
@@ -185,11 +184,26 @@ const preprocessRunMemoryExpression = (defaultMemoryMbytes: string): string => {
185184 return processedExpression ;
186185} ;
187186
187+ const getCompiledExpression = ( expression : string , cache : LruCache < EvalFunction > | undefined ) : EvalFunction => {
188+ if ( ! cache ) {
189+ return compile ( expression ) ;
190+ }
191+
192+ let compiledExpression = cache . get ( expression ) ;
193+
194+ if ( ! compiledExpression ) {
195+ compiledExpression = compile ( expression ) ;
196+ cache . add ( expression , compiledExpression ! ) ;
197+ }
198+
199+ return compiledExpression ;
200+ } ;
201+
188202/**
189203 * Evaluates a dynamic memory expression string using the provided context.
190204 * Result is rounded to the closest power of 2 and clamped within allowed limits.
191205 *
192- * @param defaultMemoryMbytes The string expression to evaluate (e.g., " get(input, 'size ', 10) * 1024" ).
206+ * @param defaultMemoryMbytes The string expression to evaluate (e.g., ` get(input, 'urls.length ', 10) * 1024` for `input = { urls: ['url1', 'url2'] }` ).
193207 * @param context The `MemoryEvaluationContext` (containing `input` and `runOptions`) available to the expression.
194208 * @returns The calculated memory value rounded to the closest power of 2 clamped within allowed limits.
195209*/
@@ -198,33 +212,22 @@ export const calculateRunDynamicMemory = (
198212 context : MemoryEvaluationContext ,
199213 options : { cache : LruCache < EvalFunction > } | undefined = undefined ,
200214) => {
201- if ( defaultMemoryMbytes . length > DEFAULT_MEMORY_MBYTES_MAX_CHARS ) {
202- throw new Error ( `The defaultMemoryMbytes expression is too long. Max length is ${ DEFAULT_MEMORY_MBYTES_MAX_CHARS } characters.` ) ;
215+ if ( defaultMemoryMbytes . length > DEFAULT_MEMORY_MBYTES_EXPRESSION_MAX_LENGTH ) {
216+ throw new Error ( `The defaultMemoryMbytes expression is too long. Max length is ${ DEFAULT_MEMORY_MBYTES_EXPRESSION_MAX_LENGTH } characters.` ) ;
203217 }
204218
205219 // Replaces all occurrences of {{variable}} with variable
206220 // e.g., "{{runOptions.memoryMbytes}} + 1024" becomes "runOptions.memoryMbytes + 1024"
207- const preprocessedExpression = preprocessRunMemoryExpression ( defaultMemoryMbytes ) ;
221+ const preprocessedExpression = processTemplateVariables ( defaultMemoryMbytes ) ;
208222
209223 const preparedContext = {
210224 ...context ,
211225 get : customGetFunc ,
212226 } ;
213227
214- let finalResult : number | { entries : number [ ] } ;
215-
216- if ( options ?. cache ) {
217- let compiledExpr = options . cache . get ( preprocessedExpression ) ;
228+ const compiledExpression = getCompiledExpression ( preprocessedExpression , options ?. cache ) ;
218229
219- if ( ! compiledExpr ) {
220- compiledExpr = limitedCompile ( preprocessedExpression ) ;
221- options . cache . add ( preprocessedExpression , compiledExpr ! ) ;
222- }
223-
224- finalResult = compiledExpr . evaluate ( preparedContext ) ;
225- } else {
226- finalResult = limitedEvaluate ( preprocessedExpression , preparedContext ) ;
227- }
230+ let finalResult : number | { entries : number [ ] } = compiledExpression . evaluate ( preparedContext ) ;
228231
229232 // Mathjs wraps multi-line expressions in an object, so we need to extract the last entry.
230233 // Note: one-line expressions return a number directly.
0 commit comments