|
| 1 | +// Copyright 2026 Google LLC |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +// [START full-sample] |
| 16 | +// Dependencies for callable functions. |
| 17 | +import 'dart:convert'; |
| 18 | +import 'dart:math'; |
| 19 | + |
| 20 | +import 'package:firebase_functions/firebase_functions.dart'; |
| 21 | +import 'package:http/http.dart' as http; |
| 22 | + |
| 23 | +/// Gets the weather from the national weather service |
| 24 | +/// https://www.weather.gov/documentation/services-web-api |
| 25 | +Future<dynamic> weatherForecastApi(num lat, num lng) async { |
| 26 | + final resp = await http.get( |
| 27 | + Uri.parse('https://api.weather.gov/points/$lat,$lng'), |
| 28 | + ); |
| 29 | + |
| 30 | + if (resp.statusCode >= 400) { |
| 31 | + return 'error: ${resp.statusCode}'; |
| 32 | + } |
| 33 | + |
| 34 | + final pointData = jsonDecode(resp.body) as Map<String, dynamic>; |
| 35 | + final forecastUrl = pointData['properties']['forecast'] as String; |
| 36 | + final forecastResp = await http.get(Uri.parse(forecastUrl)); |
| 37 | + |
| 38 | + if (forecastResp.statusCode >= 400) { |
| 39 | + return 'error: ${forecastResp.statusCode}'; |
| 40 | + } |
| 41 | + |
| 42 | + // add an artificial wait to emphasize stream-iness |
| 43 | + final randomWait = Random().nextDouble() * 1500; |
| 44 | + await Future<void>.delayed(Duration(milliseconds: randomWait.toInt())); |
| 45 | + |
| 46 | + return jsonDecode(forecastResp.body); |
| 47 | +} |
| 48 | + |
| 49 | +void main(List<String> args) async { |
| 50 | + await fireUp(args, (firebase) { |
| 51 | + // [START streaming-callable] |
| 52 | + firebase.https.onCall(name: 'getForecast', (request, response) async { |
| 53 | + final data = request.data as Map<String, Object?>?; |
| 54 | + final locations = data?['locations'] as List<dynamic>?; |
| 55 | + |
| 56 | + if (locations == null || locations.isEmpty) { |
| 57 | + throw InvalidArgumentError('Missing locations to forecast'); |
| 58 | + } |
| 59 | + |
| 60 | + // fetch forecast data for all requested locations |
| 61 | + final allRequests = locations.map((location) async { |
| 62 | + if (location case { |
| 63 | + 'latitude': num latitude, |
| 64 | + 'longitude': num longitude, |
| 65 | + }) { |
| 66 | + final forecast = await weatherForecastApi(latitude, longitude); |
| 67 | + final result = { |
| 68 | + 'latitude': latitude, |
| 69 | + 'longitude': longitude, |
| 70 | + 'forecast': forecast, |
| 71 | + }; |
| 72 | + |
| 73 | + // clients that support streaming will have each |
| 74 | + // forecast streamed to them as they complete |
| 75 | + if (request.acceptsStreaming) { |
| 76 | + await response.sendChunk(result); |
| 77 | + } |
| 78 | + |
| 79 | + return result; |
| 80 | + } |
| 81 | + |
| 82 | + throw InvalidArgumentError('Invalid location format'); |
| 83 | + }); |
| 84 | + |
| 85 | + // Return the full set of data to all clients |
| 86 | + return CallableResult(await Future.wait(allRequests)); |
| 87 | + }); |
| 88 | + // [END streaming-callable] |
| 89 | + }); |
| 90 | +} |
| 91 | + |
| 92 | +// [END full-sample] |
0 commit comments