Skip to content

Commit e253632

Browse files
committed
feat(functions): Implement Task Queue Scopes
Implemented Task Queue Scopes for Functions service in Dart. Introduced FunctionScope sealed class and updated taskQueue factory method. Handled kit fallback logic on 404 and legacy parameter deprecation warnings.
1 parent c9f6c9f commit e253632

6 files changed

Lines changed: 436 additions & 49 deletions

File tree

packages/firebase_admin_sdk/lib/src/app/environment.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,11 @@ abstract class Environment {
174174
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
175175
return env[cloudTasksEmulatorHost];
176176
}
177+
178+
/// Gets the Kit Instance ID if running within a kit.
179+
static String? getKitInstanceId() {
180+
final env =
181+
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
182+
return env['KIT_INSTANCE_ID'];
183+
}
177184
}

packages/firebase_admin_sdk/lib/src/functions/functions.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,17 @@ class Functions implements FirebaseService {
7878
/// final queue = functions.taskQueue('myFunction');
7979
/// await queue.enqueue({'data': 'value'});
8080
/// ```
81-
TaskQueue taskQueue(String functionName, {String? extensionId}) {
81+
TaskQueue taskQueue(
82+
String functionName, {
83+
@Deprecated('Use scope instead. Will be removed in a future major version.')
84+
String? extensionId,
85+
FunctionScope? scope,
86+
}) {
8287
return TaskQueue._(
8388
functionName: functionName,
8489
requestHandler: _requestHandler,
8590
extensionId: extensionId,
91+
scope: scope,
8692
);
8793
}
8894

packages/firebase_admin_sdk/lib/src/functions/functions_api.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,59 @@ class TaskOptions {
174174
/// Contains experimental features that may change in future releases.
175175
final TaskOptionsExperimental? experimental;
176176
}
177+
178+
/// Represents the scope of a function in a task queue.
179+
sealed class FunctionScope {
180+
const FunctionScope._();
181+
182+
/// Targets a function co-deployed in the same codebase/deployment context.
183+
const factory FunctionScope.current() = _CurrentFunctionScope;
184+
185+
/// Targets a global function (e.g. without kit or extension namespacing).
186+
const factory FunctionScope.global() = _GlobalFunctionScope;
187+
188+
/// Targets a function deployed within a specific Firebase Extension.
189+
const factory FunctionScope.extension(String extensionId) =
190+
_ExtensionFunctionScope;
191+
}
192+
193+
class _GlobalFunctionScope extends FunctionScope {
194+
const _GlobalFunctionScope() : super._();
195+
196+
@override
197+
String toString() => 'FunctionScope.global()';
198+
}
199+
200+
class _ExtensionFunctionScope extends FunctionScope {
201+
const _ExtensionFunctionScope(this.extensionId) : super._();
202+
203+
final String extensionId;
204+
205+
@override
206+
String toString() => 'FunctionScope.extension($extensionId)';
207+
}
208+
209+
class _KitFunctionScope extends FunctionScope {
210+
const _KitFunctionScope(this.kit) : super._();
211+
212+
final String kit;
213+
214+
@override
215+
String toString() => 'FunctionScope.kit($kit)';
216+
}
217+
218+
class _CurrentFunctionScope extends FunctionScope {
219+
const _CurrentFunctionScope() : super._();
220+
221+
@override
222+
String toString() => 'FunctionScope.current()';
223+
}
224+
225+
class _ExtensionOrKitFunctionScope extends FunctionScope {
226+
const _ExtensionOrKitFunctionScope(this.instance) : super._();
227+
228+
final String instance;
229+
230+
@override
231+
String toString() => 'FunctionScope.extensionOrKit($instance)';
232+
}

packages/firebase_admin_sdk/lib/src/functions/functions_request_handler.dart

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class FunctionsRequestHandler {
3939
Future<void> enqueue(
4040
Map<String, dynamic> data,
4141
String functionName,
42-
String? extensionId,
42+
FunctionScope scope,
4343
TaskOptions? options,
4444
) async {
4545
validateNonEmptyString(functionName, 'functionName');
@@ -54,17 +54,16 @@ class FunctionsRequestHandler {
5454

5555
validateNonEmptyString(resources.resourceId, 'resourceId');
5656

57-
// Apply extension ID prefix if provided
58-
var queueId = resources.resourceId;
59-
if (extensionId != null && extensionId.isNotEmpty) {
60-
queueId = 'ext-$extensionId-$queueId';
61-
}
57+
final (queueId, extensionOrKitId) = _resolveResourceId(
58+
resources.resourceId,
59+
scope,
60+
);
6261

6362
// Build the task
6463
final task = _buildTask(data, resources, queueId, options);
6564

6665
// Update task with proper authentication (OIDC token or Authorization header)
67-
await _updateTaskAuth(task, await _httpClient.client, extensionId);
66+
await _updateTaskAuth(task, await _httpClient.client, extensionOrKitId);
6867

6968
final parent = _httpClient.buildTasksParent(
7069
projectId: resources.projectId!,
@@ -94,7 +93,7 @@ class FunctionsRequestHandler {
9493
Future<void> delete(
9594
String id,
9695
String functionName,
97-
String? extensionId,
96+
FunctionScope scope,
9897
) async {
9998
validateNonEmptyString(functionName, 'functionName');
10099
validateNonEmptyString(id, 'id');
@@ -117,11 +116,7 @@ class FunctionsRequestHandler {
117116

118117
validateNonEmptyString(resources.resourceId, 'resourceId');
119118

120-
// Apply extension ID prefix if provided
121-
var queueId = resources.resourceId;
122-
if (extensionId != null && extensionId.isNotEmpty) {
123-
queueId = 'ext-$extensionId-$queueId';
124-
}
119+
final (queueId, _) = _resolveResourceId(resources.resourceId, scope);
125120

126121
// Build the full task name
127122
final taskName = _httpClient.buildTaskName(
@@ -143,6 +138,28 @@ class FunctionsRequestHandler {
143138
});
144139
}
145140

141+
(String resourceId, String? extensionOrKitId) _resolveResourceId(
142+
String resourceId,
143+
FunctionScope scope,
144+
) {
145+
switch (scope) {
146+
case _CurrentFunctionScope():
147+
final kitInstanceId = Environment.getKitInstanceId();
148+
if (kitInstanceId != null && kitInstanceId.isNotEmpty) {
149+
return ('kit-$kitInstanceId-$resourceId', kitInstanceId);
150+
}
151+
return (resourceId, null);
152+
case _GlobalFunctionScope():
153+
return (resourceId, null);
154+
case _ExtensionFunctionScope(:final extensionId):
155+
return ('ext-$extensionId-$resourceId', extensionId);
156+
case _KitFunctionScope(:final kit):
157+
return ('kit-$kit-$resourceId', kit);
158+
case _ExtensionOrKitFunctionScope(:final instance):
159+
return ('ext-$instance-$resourceId', instance);
160+
}
161+
}
162+
146163
/// Parses a resource name into its components.
147164
///
148165
/// Supports:
@@ -255,7 +272,7 @@ class FunctionsRequestHandler {
255272
Future<void> _updateTaskAuth(
256273
tasks2.Task task,
257274
googleapis_auth.AuthClient authClient,
258-
String? extensionId,
275+
String? extensionOrKitId,
259276
) async {
260277
final httpRequest = task.httpRequest!;
261278

@@ -271,7 +288,9 @@ class FunctionsRequestHandler {
271288
final isComputeEngine =
272289
_httpClient.app.options.credential?.serviceAccountCredentials == null;
273290

274-
if (extensionId != null && extensionId.isNotEmpty && isComputeEngine) {
291+
if (extensionOrKitId != null &&
292+
extensionOrKitId.isNotEmpty &&
293+
isComputeEngine) {
275294
// Running as extension with ComputeEngine - use ID token with Authorization header.
276295
final idToken = authClient.credentials.idToken;
277296
if (idToken != null && idToken.isNotEmpty) {

packages/firebase_admin_sdk/lib/src/functions/task_queue.dart

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,44 @@ class TaskQueue {
2323
required String functionName,
2424
required FunctionsRequestHandler requestHandler,
2525
String? extensionId,
26+
FunctionScope? scope,
2627
}) : _functionName = functionName,
27-
_requestHandler = requestHandler,
28-
_extensionId = extensionId {
28+
_requestHandler = requestHandler {
2929
validateNonEmptyString(_functionName, 'functionName');
30-
if (_extensionId != null) {
31-
validateString(_extensionId, 'extensionId');
30+
31+
if (extensionId != null && scope != null) {
32+
throw ArgumentError('Cannot set both extensionId and scope.');
33+
}
34+
35+
if (extensionId != null) {
36+
validateString(extensionId, 'extensionId');
37+
_scope = extensionId.isEmpty
38+
? const FunctionScope.current()
39+
: _ExtensionOrKitFunctionScope(extensionId);
40+
} else {
41+
_scope = scope ?? const FunctionScope.current();
42+
}
43+
44+
if (_scope is _ExtensionOrKitFunctionScope) {
45+
final instance = (_scope as _ExtensionOrKitFunctionScope).instance;
46+
final kitInstanceId = Environment.getKitInstanceId();
47+
if (kitInstanceId != null &&
48+
kitInstanceId.isNotEmpty &&
49+
kitInstanceId == instance) {
50+
_scope = _KitFunctionScope(instance);
51+
print(
52+
'Targeting your own extension or kit no longer requires a second parameter, '
53+
'which can have performance implications. Please change the call '
54+
"taskQueue('$_functionName', '$instance') to taskQueue('$_functionName') "
55+
"or taskQueue('$_functionName', scope: FunctionScope.current())",
56+
);
57+
}
3258
}
3359
}
3460

3561
final String _functionName;
3662
final FunctionsRequestHandler _requestHandler;
37-
final String? _extensionId;
63+
late FunctionScope _scope;
3864

3965
/// Enqueues a task with the given [data] payload.
4066
///
@@ -59,8 +85,28 @@ class TaskQueue {
5985
/// ```
6086
///
6187
/// Throws [FirebaseFunctionsAdminException] if the request fails.
62-
Future<void> enqueue(Map<String, dynamic> data, [TaskOptions? options]) {
63-
return _requestHandler.enqueue(data, _functionName, _extensionId, options);
88+
Future<void> enqueue(
89+
Map<String, dynamic> data, [
90+
TaskOptions? options,
91+
]) async {
92+
final currentScope = _scope;
93+
if (currentScope is! _ExtensionOrKitFunctionScope) {
94+
await _requestHandler.enqueue(data, _functionName, currentScope, options);
95+
return;
96+
}
97+
98+
try {
99+
await _requestHandler.enqueue(data, _functionName, currentScope, options);
100+
} on FirebaseFunctionsAdminException catch (err) {
101+
if (err.errorCode != FunctionsClientErrorCode.notFound) {
102+
rethrow;
103+
}
104+
final tempKitScope = _KitFunctionScope(currentScope.instance);
105+
await _requestHandler.enqueue(data, _functionName, tempKitScope, options);
106+
// Only upgrade the stateful scope to kit if the retry request succeeds
107+
_scope = tempKitScope;
108+
_logFallbackWarning(_functionName, currentScope.instance);
109+
}
64110
}
65111

66112
/// Deletes a task from the queue by its [id].
@@ -74,7 +120,40 @@ class TaskQueue {
74120
/// ```
75121
///
76122
/// Throws [FirebaseFunctionsAdminException] if the request fails.
77-
Future<void> delete(String id) {
78-
return _requestHandler.delete(id, _functionName, _extensionId);
123+
Future<void> delete(String id) async {
124+
try {
125+
final currentScope = _scope;
126+
if (currentScope is! _ExtensionOrKitFunctionScope) {
127+
await _requestHandler.delete(id, _functionName, currentScope);
128+
return;
129+
}
130+
131+
try {
132+
await _requestHandler.delete(id, _functionName, currentScope);
133+
} on FirebaseFunctionsAdminException catch (err) {
134+
if (err.errorCode != FunctionsClientErrorCode.notFound) {
135+
rethrow;
136+
}
137+
// Not found, try fallback to kit scope.
138+
final tempKitScope = _KitFunctionScope(currentScope.instance);
139+
await _requestHandler.delete(id, _functionName, tempKitScope);
140+
// Only upgrade the stateful scope to kit if the retry request succeeds
141+
_scope = tempKitScope;
142+
_logFallbackWarning(_functionName, currentScope.instance);
143+
}
144+
} on FirebaseFunctionsAdminException catch (err) {
145+
if (err.errorCode != FunctionsClientErrorCode.notFound) {
146+
rethrow;
147+
}
148+
// Swallow notFound errors as delete is idempotent.
149+
}
150+
}
151+
152+
void _logFallbackWarning(String functionName, String instance) {
153+
print(
154+
'Targeting kit $instance with the legacy extensions API, '
155+
'which has performance implications. Please change the call '
156+
"taskQueue('$functionName', '$instance') to taskQueue('$functionName')",
157+
);
79158
}
80159
}

0 commit comments

Comments
 (0)