Skip to content

Commit 8d29820

Browse files
authored
feat: add Functions Task Queue API with emulator support and tests (#116)
* refactor(messaging): rename _toProto methods to _toRequest for message serialization * feat(messaging): add topic subscription and unsubscription APIs with validation and tests * wip: add Cloud Run example server with messaging and token verification APIs * feat(messaging): complete implementation with tests and bug fixes * chore: lint errors * chore: fix lint errors * feat(functions): add Cloud Functions Task Queue admin API with validation and error handling * refactor: rename client param to api x_http_client.dart * wip: add Cloud Tasks emulator support with URL rewriting and env detection * test: add integration and unit tests for Task Queue, enqueue, delete * test: add comprehensive unit tests for Functions TaskQueue, validation, and error handling * fix: use existing test service account credentials - add functions ts example project for functionsExample method in example/main.dart * chore: add package-lock.json to .gitignore * refactor: update taskQueue to use named extensionId parameter and update tests * chore: add doc/api to .gitignore
1 parent b76c3c8 commit 8d29820

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2978
-100
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ packages/dart_firebase_admin/test/client/package-lock.json
88

99
build
1010
coverage
11+
doc/api
1112

1213
.DS_Store
1314
.atom/
@@ -33,4 +34,4 @@ pubspec.lock
3334

3435
service-account.json
3536

36-
**/pubspec_overrides.yaml
37+
**/pubspec_overrides.yaml
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"projects": {
3+
"default": "dart-firebase-admin"
4+
}
5+
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
service-account-key.json
1+
service-account-key.json
2+
3+
# Test functions artifacts
4+
test/functions/node_modules/
5+
test/functions/lib/
6+
test/functions/package-lock.json
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module.exports = {
2+
root: true,
3+
env: {
4+
es6: true,
5+
node: true,
6+
},
7+
extends: [
8+
"eslint:recommended",
9+
"plugin:import/errors",
10+
"plugin:import/warnings",
11+
"plugin:import/typescript",
12+
"google",
13+
"plugin:@typescript-eslint/recommended",
14+
],
15+
parser: "@typescript-eslint/parser",
16+
parserOptions: {
17+
project: ["tsconfig.json", "tsconfig.dev.json"],
18+
sourceType: "module",
19+
},
20+
ignorePatterns: [
21+
"/lib/**/*", // Ignore built files.
22+
"/generated/**/*", // Ignore generated files.
23+
],
24+
plugins: [
25+
"@typescript-eslint",
26+
"import",
27+
],
28+
rules: {
29+
"quotes": ["error", "double"],
30+
"import/no-unresolved": 0,
31+
"indent": ["error", 2],
32+
},
33+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Compiled JavaScript files
2+
lib/**/*.js
3+
lib/**/*.js.map
4+
5+
# TypeScript v1 declaration files
6+
typings/
7+
8+
# Node.js dependency directory
9+
node_modules/
10+
*.local
11+
package-lock.json
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "functions",
3+
"scripts": {
4+
"lint": "eslint --ext .js,.ts .",
5+
"build": "tsc",
6+
"build:watch": "tsc --watch",
7+
"serve": "npm run build && firebase emulators:start --only functions",
8+
"shell": "npm run build && firebase functions:shell",
9+
"start": "npm run shell",
10+
"deploy": "firebase deploy --only functions",
11+
"logs": "firebase functions:log"
12+
},
13+
"engines": {
14+
"node": "22"
15+
},
16+
"main": "lib/index.js",
17+
"dependencies": {
18+
"firebase-admin": "^12.6.0",
19+
"firebase-functions": "^6.0.1"
20+
},
21+
"devDependencies": {
22+
"@typescript-eslint/eslint-plugin": "^5.12.0",
23+
"@typescript-eslint/parser": "^5.12.0",
24+
"eslint": "^8.9.0",
25+
"eslint-config-google": "^0.14.0",
26+
"eslint-plugin-import": "^2.25.4",
27+
"firebase-functions-test": "^3.1.0",
28+
"typescript": "^4.9.0"
29+
},
30+
"private": true
31+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Import function triggers from their respective submodules:
3+
*
4+
* import {onCall} from "firebase-functions/v2/https";
5+
* import {onDocumentWritten} from "firebase-functions/v2/firestore";
6+
*
7+
* See a full list of supported triggers at https://firebase.google.com/docs/functions
8+
*/
9+
10+
import {onTaskDispatched} from "firebase-functions/v2/tasks";
11+
12+
// Start writing functions
13+
// https://firebase.google.com/docs/functions/typescript
14+
15+
export const helloWorld = onTaskDispatched(
16+
{
17+
retryConfig: {
18+
maxAttempts: 5,
19+
minBackoffSeconds: 60,
20+
},
21+
rateLimits: {
22+
maxConcurrentDispatches: 6,
23+
},
24+
},
25+
async (req) => {
26+
console.log("Task received:", req.data);
27+
}
28+
);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"include": [
3+
".eslintrc.js"
4+
]
5+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"module": "NodeNext",
4+
"esModuleInterop": true,
5+
"moduleResolution": "nodenext",
6+
"noImplicitReturns": true,
7+
"noUnusedLocals": true,
8+
"outDir": "lib",
9+
"sourceMap": true,
10+
"strict": true,
11+
"target": "es2017"
12+
},
13+
"compileOnSave": true,
14+
"include": [
15+
"src"
16+
]
17+
}

packages/dart_firebase_admin/example/lib/main.dart

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
11
import 'package:dart_firebase_admin/auth.dart';
22
import 'package:dart_firebase_admin/dart_firebase_admin.dart';
33
import 'package:dart_firebase_admin/firestore.dart';
4+
import 'package:dart_firebase_admin/functions.dart';
45
import 'package:dart_firebase_admin/messaging.dart';
56

67
Future<void> main() async {
78
final admin = FirebaseApp.initializeApp();
8-
await authExample(admin);
9-
await firestoreExample(admin);
10-
await projectConfigExample(admin);
9+
10+
// Uncomment to run auth example
11+
// await authExample(admin);
12+
13+
// Uncomment to run firestore example
14+
// await firestoreExample(admin);
15+
16+
// Uncomment to run project config example
17+
// await projectConfigExample(admin);
1118

1219
// Uncomment to run tenant example (requires Identity Platform upgrade)
1320
// await tenantExample(admin);
1421

1522
// Uncomment to run messaging example (requires valid fcm token)
1623
// await messagingExample(admin);
1724

25+
// Uncomment to run functions example
26+
// await functionsExample(admin);
27+
1828
await admin.close();
1929
}
2030

31+
// ignore: unreachable_from_main
2132
Future<void> authExample(FirebaseApp admin) async {
2233
print('\n### Auth Example ###\n');
2334

@@ -47,6 +58,7 @@ Future<void> authExample(FirebaseApp admin) async {
4758
}
4859
}
4960

61+
// ignore: unreachable_from_main
5062
Future<void> firestoreExample(FirebaseApp admin) async {
5163
print('\n### Firestore Example ###\n');
5264

@@ -64,6 +76,7 @@ Future<void> firestoreExample(FirebaseApp admin) async {
6476
}
6577
}
6678

79+
// ignore: unreachable_from_main
6780
Future<void> projectConfigExample(FirebaseApp admin) async {
6881
print('\n### Project Config Example ###\n');
6982

@@ -379,3 +392,83 @@ Future<void> messagingExample(FirebaseApp admin) async {
379392
print('> Error sending platform-specific message: $e');
380393
}
381394
}
395+
396+
/// Functions example prerequisites:
397+
/// 1) Run `npm run build` in `example_functions_ts` to generate `index.js`.
398+
/// 2) From the example directory root (with `firebase.json` and `.firebaserc`),
399+
/// start emulators with `firebase emulators:start`.
400+
/// 3) Run `dart_firebase_admin/packages/dart_firebase_admin/example/run_with_emulator.sh`.
401+
// ignore: unreachable_from_main
402+
Future<void> functionsExample(FirebaseApp admin) async {
403+
print('\n### Functions Example ###\n');
404+
405+
final functions = Functions(admin);
406+
407+
// Get a task queue reference
408+
// The function name should match an existing Cloud Function or queue name
409+
final taskQueue = functions.taskQueue('helloWorld');
410+
411+
// Example 1: Enqueue a simple task
412+
try {
413+
print('> Enqueuing a simple task...\n');
414+
await taskQueue.enqueue({
415+
'userId': 'user-123',
416+
'action': 'sendWelcomeEmail',
417+
'timestamp': DateTime.now().toIso8601String(),
418+
});
419+
print('Task enqueued successfully!\n');
420+
} on FirebaseFunctionsAdminException catch (e) {
421+
print('> Functions error: ${e.code} - ${e.message}\n');
422+
} catch (e) {
423+
print('> Error enqueuing task: $e\n');
424+
}
425+
426+
// Example 2: Enqueue with delay (1 hour from now)
427+
try {
428+
print('> Enqueuing a delayed task...\n');
429+
await taskQueue.enqueue(
430+
{'action': 'cleanupTempFiles'},
431+
TaskOptions(schedule: DelayDelivery(3600)), // 1 hour delay
432+
);
433+
print('Delayed task enqueued successfully!\n');
434+
} on FirebaseFunctionsAdminException catch (e) {
435+
print('> Functions error: ${e.code} - ${e.message}\n');
436+
}
437+
438+
// Example 3: Enqueue at specific time
439+
try {
440+
print('> Enqueuing a scheduled task...\n');
441+
final scheduledTime = DateTime.now().add(const Duration(minutes: 30));
442+
await taskQueue.enqueue({
443+
'action': 'sendReport',
444+
}, TaskOptions(schedule: AbsoluteDelivery(scheduledTime)));
445+
print('Scheduled task enqueued for: $scheduledTime\n');
446+
} on FirebaseFunctionsAdminException catch (e) {
447+
print('> Functions error: ${e.code} - ${e.message}\n');
448+
}
449+
450+
// Example 4: Enqueue with custom task ID (for deduplication)
451+
try {
452+
print('> Enqueuing a task with custom ID...\n');
453+
await taskQueue.enqueue({
454+
'orderId': 'order-456',
455+
'action': 'processPayment',
456+
}, TaskOptions(id: 'payment-order-456'));
457+
print('Task with custom ID enqueued!\n');
458+
} on FirebaseFunctionsAdminException catch (e) {
459+
if (e.errorCode == FunctionsClientErrorCode.taskAlreadyExists) {
460+
print('> Task with this ID already exists (deduplication)\n');
461+
} else {
462+
print('> Functions error: ${e.code} - ${e.message}\n');
463+
}
464+
}
465+
466+
// Example 5: Delete a task
467+
try {
468+
print('> Deleting task...\n');
469+
await taskQueue.delete('payment-order-456');
470+
print('Task deleted successfully!\n');
471+
} on FirebaseFunctionsAdminException catch (e) {
472+
print('> Functions error: ${e.code} - ${e.message}\n');
473+
}
474+
}

0 commit comments

Comments
 (0)