@@ -38,6 +38,8 @@ class _TypeCheckers {
3838 TypeChecker .fromRuntime (ff.PerformanceNamespace );
3939 static final identityNamespace =
4040 TypeChecker .fromRuntime (ff.IdentityNamespace );
41+ static final schedulerNamespace =
42+ TypeChecker .fromRuntime (ff.SchedulerNamespace );
4143}
4244
4345/// The main builder that generates functions.yaml.
@@ -186,6 +188,13 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
186188 _extractIdentityFunction (node, methodName);
187189 }
188190
191+ // Check for Scheduler function declarations
192+ if (target != null && _isSchedulerNamespace (target)) {
193+ if (methodName == 'onSchedule' ) {
194+ _extractSchedulerFunction (node);
195+ }
196+ }
197+
189198 // Check for parameter definitions (top-level function calls with no target)
190199 if (target == null && _isParamDefinition (methodName)) {
191200 _extractParameterFromMethod (node, methodName);
@@ -339,6 +348,13 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
339348 return _TypeCheckers .identityNamespace.isExactlyType (staticType);
340349 }
341350
351+ /// Checks if the target is firebase.scheduler.
352+ bool _isSchedulerNamespace (Expression target) {
353+ final staticType = target.staticType;
354+ if (staticType == null ) return false ;
355+ return _TypeCheckers .schedulerNamespace.isExactlyType (staticType);
356+ }
357+
342358 /// Checks if this is a parameter definition function.
343359 bool _isParamDefinition (String name) =>
344360 name == 'defineString' ||
@@ -639,6 +655,101 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
639655 );
640656 }
641657
658+ /// Extracts a Scheduler function declaration.
659+ void _extractSchedulerFunction (MethodInvocation node) {
660+ // Extract schedule from named argument
661+ final scheduleArg = _findNamedArg (node, 'schedule' );
662+ if (scheduleArg == null ) return ;
663+
664+ final schedule = _extractStringLiteral (scheduleArg);
665+ if (schedule == null ) return ;
666+
667+ // Generate function name from schedule (matching runtime behavior)
668+ final sanitized = schedule
669+ .replaceAll (' ' , '_' )
670+ .replaceAll ('*' , '' )
671+ .replaceAll ('/' , '' )
672+ .replaceAll ('-' , '' )
673+ .replaceAll (',' , '' );
674+ final functionName = 'onSchedule_$sanitized ' ;
675+
676+ // Extract options if present
677+ final optionsArg = _findNamedArg (node, 'options' );
678+ String ? timeZone;
679+ Map <String , dynamic >? retryConfig;
680+
681+ if (optionsArg is InstanceCreationExpression ) {
682+ timeZone = _extractSchedulerTimeZone (optionsArg);
683+ retryConfig = _extractRetryConfig (optionsArg);
684+ }
685+
686+ endpoints[functionName] = _EndpointSpec (
687+ name: functionName,
688+ type: 'scheduler' ,
689+ schedule: schedule,
690+ timeZone: timeZone,
691+ retryConfig: retryConfig,
692+ options: optionsArg is InstanceCreationExpression ? optionsArg : null ,
693+ variableToParamName: _variableToParamName,
694+ );
695+ }
696+
697+ /// Extracts timeZone from ScheduleOptions.
698+ String ? _extractSchedulerTimeZone (InstanceCreationExpression node) {
699+ final timeZoneArg = node.argumentList.arguments
700+ .whereType <NamedExpression >()
701+ .where ((e) => e.name.label.name == 'timeZone' )
702+ .map ((e) => e.expression)
703+ .firstOrNull;
704+
705+ if (timeZoneArg is InstanceCreationExpression ) {
706+ // TimeZone('America/New_York')
707+ final args = timeZoneArg.argumentList.arguments;
708+ if (args.isNotEmpty && args.first is StringLiteral ) {
709+ return (args.first as StringLiteral ).stringValue;
710+ }
711+ }
712+ return null ;
713+ }
714+
715+ /// Extracts RetryConfig from ScheduleOptions.
716+ Map <String , dynamic >? _extractRetryConfig (InstanceCreationExpression node) {
717+ final retryConfigArg = node.argumentList.arguments
718+ .whereType <NamedExpression >()
719+ .where ((e) => e.name.label.name == 'retryConfig' )
720+ .map ((e) => e.expression)
721+ .firstOrNull;
722+
723+ if (retryConfigArg is ! InstanceCreationExpression ) return null ;
724+
725+ final config = < String , dynamic > {};
726+
727+ for (final arg in retryConfigArg.argumentList.arguments) {
728+ if (arg is ! NamedExpression ) continue ;
729+
730+ final fieldName = arg.name.label.name;
731+ final value = _extractRetryConfigValue (arg.expression);
732+ if (value != null ) {
733+ config[fieldName] = value;
734+ }
735+ }
736+
737+ return config.isEmpty ? null : config;
738+ }
739+
740+ /// Extracts a value from a retry config option.
741+ dynamic _extractRetryConfigValue (Expression expression) {
742+ if (expression is InstanceCreationExpression ) {
743+ final args = expression.argumentList.arguments;
744+ if (args.isNotEmpty) {
745+ final first = args.first;
746+ if (first is IntegerLiteral ) return first.value;
747+ if (first is DoubleLiteral ) return first.value;
748+ }
749+ }
750+ return null ;
751+ }
752+
642753 /// Extracts a boolean field from an InstanceCreationExpression.
643754 bool ? _extractBoolField (InstanceCreationExpression node, String fieldName) {
644755 final arg = node.argumentList.arguments
@@ -888,11 +999,14 @@ class _EndpointSpec {
888999 this .idToken,
8891000 this .accessToken,
8901001 this .refreshToken,
1002+ this .schedule,
1003+ this .timeZone,
1004+ this .retryConfig,
8911005 this .options,
8921006 this .variableToParamName = const {},
8931007 });
8941008 final String name;
895- // 'https', 'callable', 'pubsub', 'firestore', 'database', 'alert', 'blocking'
1009+ // 'https', 'callable', 'pubsub', 'firestore', 'database', 'alert', 'blocking', 'scheduler'
8961010 final String type;
8971011 final String ? topic; // For Pub/Sub functions
8981012 final String ? firestoreEventType; // For Firestore: onDocumentCreated, etc.
@@ -908,6 +1022,9 @@ class _EndpointSpec {
9081022 final bool ? idToken; // For Identity: pass ID token
9091023 final bool ? accessToken; // For Identity: pass access token
9101024 final bool ? refreshToken; // For Identity: pass refresh token
1025+ final String ? schedule; // For Scheduler: cron expression
1026+ final String ? timeZone; // For Scheduler: timezone
1027+ final Map <String , dynamic >? retryConfig; // For Scheduler: retry configuration
9111028 final InstanceCreationExpression ? options;
9121029 final Map <String , String > variableToParamName;
9131030
@@ -1458,6 +1575,13 @@ String _generateYaml(
14581575 buffer.writeln (' - api: "identitytoolkit.googleapis.com"' );
14591576 buffer.writeln (' reason: "Needed for auth blocking functions"' );
14601577 }
1578+ // Add cloudscheduler API if there are scheduler functions
1579+ final hasSchedulerFunctions =
1580+ endpoints.values.any ((e) => e.type == 'scheduler' );
1581+ if (hasSchedulerFunctions) {
1582+ buffer.writeln (' - api: "cloudscheduler.googleapis.com"' );
1583+ buffer.writeln (' reason: "Needed for scheduled functions"' );
1584+ }
14611585 buffer.writeln ();
14621586
14631587 // Generate endpoints section
@@ -1685,6 +1809,37 @@ String _generateYaml(
16851809 } else {
16861810 buffer.writeln (' options: {}' );
16871811 }
1812+ } else if (endpoint.type == 'scheduler' && endpoint.schedule != null ) {
1813+ buffer.writeln (' scheduleTrigger:' );
1814+ buffer.writeln (' schedule: "${endpoint .schedule }"' );
1815+ if (endpoint.timeZone != null ) {
1816+ buffer.writeln (' timeZone: "${endpoint .timeZone }"' );
1817+ }
1818+ if (endpoint.retryConfig != null && endpoint.retryConfig! .isNotEmpty) {
1819+ buffer.writeln (' retryConfig:' );
1820+ final config = endpoint.retryConfig! ;
1821+ if (config.containsKey ('retryCount' )) {
1822+ buffer.writeln (' retryCount: ${config ['retryCount' ]}' );
1823+ }
1824+ if (config.containsKey ('maxRetrySeconds' )) {
1825+ buffer.writeln (
1826+ ' maxRetrySeconds: ${config ['maxRetrySeconds' ]}' ,
1827+ );
1828+ }
1829+ if (config.containsKey ('minBackoffSeconds' )) {
1830+ buffer.writeln (
1831+ ' minBackoffSeconds: ${config ['minBackoffSeconds' ]}' ,
1832+ );
1833+ }
1834+ if (config.containsKey ('maxBackoffSeconds' )) {
1835+ buffer.writeln (
1836+ ' maxBackoffSeconds: ${config ['maxBackoffSeconds' ]}' ,
1837+ );
1838+ }
1839+ if (config.containsKey ('maxDoublings' )) {
1840+ buffer.writeln (' maxDoublings: ${config ['maxDoublings' ]}' );
1841+ }
1842+ }
16881843 }
16891844 }
16901845 }
0 commit comments