Skip to content

Commit

Permalink
Support streaming streaming responses for callable functions. (#8609)
Browse files Browse the repository at this point in the history
The new .stream() API allows the client to consume streaming responses from the WIP streaming callable functions in Firebase Functions Node.js SDK.

When client makes a request to the callable function w/ header Accept: text/event-stream, the callable function responds with response chunks in Server-Sent Event format.

The sdk changes here abstracts over the wire-protocol by parsing the response chunks and returning an instance of a AsyncIterable to consume to data:

import { getFunctions, httpsCallable } from "firebase/functions";

const functions = getFunctions();
const generateText = httpsCallable(functions, 'generateText');
const resp = await generateText.stream(
  { text: 'What is your favorite Firebase service and why?' },
  { signal: AbortSignal.timeout(60_000) },
);
try {
  for await (const message of resp.stream) {
     console.log(message); // prints "foo", "bar"
  }
  console.log(await resp.data) // prints "foo bar"
} catch (e) {
  // FirebaseError(code='cancelled', message='Request was cancelled.');
  console.error(e)
}
  • Loading branch information
taeold authored Dec 4, 2024
1 parent c540ba9 commit f05509e
Show file tree
Hide file tree
Showing 14 changed files with 918 additions and 61 deletions.
6 changes: 6 additions & 0 deletions .changeset/bright-scissors-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/functions': minor
'firebase': minor
---

Add `.stream()` api for callable functions for consuming streaming responses.
25 changes: 22 additions & 3 deletions common/api-review/functions.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ export type FunctionsErrorCodeCore = 'ok' | 'cancelled' | 'unknown' | 'invalid-a
export function getFunctions(app?: FirebaseApp, regionOrCustomDomain?: string): Functions;

// @public
export type HttpsCallable<RequestData = unknown, ResponseData = unknown> = (data?: RequestData | null) => Promise<HttpsCallableResult<ResponseData>>;
export interface HttpsCallable<RequestData = unknown, ResponseData = unknown, StreamData = unknown> {
// (undocumented)
(data?: RequestData | null): Promise<HttpsCallableResult<ResponseData>>;
// (undocumented)
stream: (data?: RequestData | null, options?: HttpsCallableStreamOptions) => Promise<HttpsCallableStreamResult<ResponseData, StreamData>>;
}

// @public
export function httpsCallable<RequestData = unknown, ResponseData = unknown>(functionsInstance: Functions, name: string, options?: HttpsCallableOptions): HttpsCallable<RequestData, ResponseData>;
export function httpsCallable<RequestData = unknown, ResponseData = unknown, StreamData = unknown>(functionsInstance: Functions, name: string, options?: HttpsCallableOptions): HttpsCallable<RequestData, ResponseData, StreamData>;

// @public
export function httpsCallableFromURL<RequestData = unknown, ResponseData = unknown>(functionsInstance: Functions, url: string, options?: HttpsCallableOptions): HttpsCallable<RequestData, ResponseData>;
export function httpsCallableFromURL<RequestData = unknown, ResponseData = unknown, StreamData = unknown>(functionsInstance: Functions, url: string, options?: HttpsCallableOptions): HttpsCallable<RequestData, ResponseData, StreamData>;

// @public
export interface HttpsCallableOptions {
Expand All @@ -54,5 +59,19 @@ export interface HttpsCallableResult<ResponseData = unknown> {
readonly data: ResponseData;
}

// @public
export interface HttpsCallableStreamOptions {
limitedUseAppCheckTokens?: boolean;
signal?: AbortSignal;
}

// @public
export interface HttpsCallableStreamResult<ResponseData = unknown, StreamData = unknown> {
// (undocumented)
readonly data: Promise<ResponseData>;
// (undocumented)
readonly stream: AsyncIterable<StreamData>;
}


```
1 change: 1 addition & 0 deletions config/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Uncomment this if you'd like others to create their own Firebase project.
6 changes: 6 additions & 0 deletions docs-devsite/_toc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,16 @@ toc:
path: /docs/reference/js/functions.functions.md
- title: FunctionsError
path: /docs/reference/js/functions.functionserror.md
- title: HttpsCallable
path: /docs/reference/js/functions.httpscallable.md
- title: HttpsCallableOptions
path: /docs/reference/js/functions.httpscallableoptions.md
- title: HttpsCallableResult
path: /docs/reference/js/functions.httpscallableresult.md
- title: HttpsCallableStreamOptions
path: /docs/reference/js/functions.httpscallablestreamoptions.md
- title: HttpsCallableStreamResult
path: /docs/reference/js/functions.httpscallablestreamresult.md
- title: installations
path: /docs/reference/js/installations.md
section:
Expand Down
33 changes: 33 additions & 0 deletions docs-devsite/functions.httpscallable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# HttpsCallable interface
A reference to a "callable" HTTP trigger in Cloud Functions.

<b>Signature:</b>

```typescript
export interface HttpsCallable<RequestData = unknown, ResponseData = unknown, StreamData = unknown>
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [stream](./functions.httpscallable.md#httpscallablestream) | (data?: RequestData \| null, options?: [HttpsCallableStreamOptions](./functions.httpscallablestreamoptions.md#httpscallablestreamoptions_interface)<!-- -->) =&gt; Promise&lt;[HttpsCallableStreamResult](./functions.httpscallablestreamresult.md#httpscallablestreamresult_interface)<!-- -->&lt;ResponseData, StreamData&gt;&gt; | |

## HttpsCallable.stream

<b>Signature:</b>

```typescript
stream: (data?: RequestData | null, options?: HttpsCallableStreamOptions) => Promise<HttpsCallableStreamResult<ResponseData, StreamData>>;
```
4 changes: 2 additions & 2 deletions docs-devsite/functions.httpscallableoptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ export interface HttpsCallableOptions

| Property | Type | Description |
| --- | --- | --- |
| [limitedUseAppCheckTokens](./functions.httpscallableoptions.md#httpscallableoptionslimiteduseappchecktokens) | boolean | If set to true, uses limited-use App Check token for callable function requests from this instance of [Functions](./functions.functions.md#functions_interface)<!-- -->. You must use limited-use tokens to call functions with replay protection enabled. By default, this is false. |
| [limitedUseAppCheckTokens](./functions.httpscallableoptions.md#httpscallableoptionslimiteduseappchecktokens) | boolean | If set to true, uses a limited-use App Check token for callable function requests from this instance of [Functions](./functions.functions.md#functions_interface)<!-- -->. You must use limited-use tokens to call functions with replay protection enabled. By default, this is false. |
| [timeout](./functions.httpscallableoptions.md#httpscallableoptionstimeout) | number | Time in milliseconds after which to cancel if there is no response. Default is 70000. |

## HttpsCallableOptions.limitedUseAppCheckTokens

If set to true, uses limited-use App Check token for callable function requests from this instance of [Functions](./functions.functions.md#functions_interface)<!-- -->. You must use limited-use tokens to call functions with replay protection enabled. By default, this is false.
If set to true, uses a limited-use App Check token for callable function requests from this instance of [Functions](./functions.functions.md#functions_interface)<!-- -->. You must use limited-use tokens to call functions with replay protection enabled. By default, this is false.

<b>Signature:</b>

Expand Down
46 changes: 46 additions & 0 deletions docs-devsite/functions.httpscallablestreamoptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# HttpsCallableStreamOptions interface
An interface for metadata about how a stream call should be executed.

<b>Signature:</b>

```typescript
export interface HttpsCallableStreamOptions
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [limitedUseAppCheckTokens](./functions.httpscallablestreamoptions.md#httpscallablestreamoptionslimiteduseappchecktokens) | boolean | If set to true, uses a limited-use App Check token for callable function requests from this instance of [Functions](./functions.functions.md#functions_interface)<!-- -->. You must use limited-use tokens to call functions with replay protection enabled. By default, this is false. |
| [signal](./functions.httpscallablestreamoptions.md#httpscallablestreamoptionssignal) | AbortSignal | An <code>AbortSignal</code> that can be used to cancel the streaming response. When the signal is aborted, the underlying HTTP connection will be terminated. |

## HttpsCallableStreamOptions.limitedUseAppCheckTokens

If set to true, uses a limited-use App Check token for callable function requests from this instance of [Functions](./functions.functions.md#functions_interface)<!-- -->. You must use limited-use tokens to call functions with replay protection enabled. By default, this is false.

<b>Signature:</b>

```typescript
limitedUseAppCheckTokens?: boolean;
```

## HttpsCallableStreamOptions.signal

An `AbortSignal` that can be used to cancel the streaming response. When the signal is aborted, the underlying HTTP connection will be terminated.

<b>Signature:</b>

```typescript
signal?: AbortSignal;
```
42 changes: 42 additions & 0 deletions docs-devsite/functions.httpscallablestreamresult.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# HttpsCallableStreamResult interface
An `HttpsCallableStreamResult` wraps a single streaming result from a function call.

<b>Signature:</b>

```typescript
export interface HttpsCallableStreamResult<ResponseData = unknown, StreamData = unknown>
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [data](./functions.httpscallablestreamresult.md#httpscallablestreamresultdata) | Promise&lt;ResponseData&gt; | |
| [stream](./functions.httpscallablestreamresult.md#httpscallablestreamresultstream) | AsyncIterable&lt;StreamData&gt; | |

## HttpsCallableStreamResult.data

<b>Signature:</b>

```typescript
readonly data: Promise<ResponseData>;
```

## HttpsCallableStreamResult.stream

<b>Signature:</b>

```typescript
readonly stream: AsyncIterable<StreamData>;
```
22 changes: 7 additions & 15 deletions docs-devsite/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,18 @@ Cloud Functions for Firebase
| Interface | Description |
| --- | --- |
| [Functions](./functions.functions.md#functions_interface) | A <code>Functions</code> instance. |
| [HttpsCallable](./functions.httpscallable.md#httpscallable_interface) | A reference to a "callable" HTTP trigger in Cloud Functions. |
| [HttpsCallableOptions](./functions.httpscallableoptions.md#httpscallableoptions_interface) | An interface for metadata about how calls should be executed. |
| [HttpsCallableResult](./functions.httpscallableresult.md#httpscallableresult_interface) | An <code>HttpsCallableResult</code> wraps a single result from a function call. |
| [HttpsCallableStreamOptions](./functions.httpscallablestreamoptions.md#httpscallablestreamoptions_interface) | An interface for metadata about how a stream call should be executed. |
| [HttpsCallableStreamResult](./functions.httpscallablestreamresult.md#httpscallablestreamresult_interface) | An <code>HttpsCallableStreamResult</code> wraps a single streaming result from a function call. |

## Type Aliases

| Type Alias | Description |
| --- | --- |
| [FunctionsErrorCode](./functions.md#functionserrorcode) | The set of Firebase Functions status codes. The codes are the same at the ones exposed by gRPC here: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md<!-- -->Possible values: - 'cancelled': The operation was cancelled (typically by the caller). - 'unknown': Unknown error or an error from a different error domain. - 'invalid-argument': Client specified an invalid argument. Note that this differs from 'failed-precondition'. 'invalid-argument' indicates arguments that are problematic regardless of the state of the system (e.g. an invalid field name). - 'deadline-exceeded': Deadline expired before operation could complete. For operations that change the state of the system, this error may be returned even if the operation has completed successfully. For example, a successful response from a server could have been delayed long enough for the deadline to expire. - 'not-found': Some requested document was not found. - 'already-exists': Some document that we attempted to create already exists. - 'permission-denied': The caller does not have permission to execute the specified operation. - 'resource-exhausted': Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. - 'failed-precondition': Operation was rejected because the system is not in a state required for the operation's execution. - 'aborted': The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. - 'out-of-range': Operation was attempted past the valid range. - 'unimplemented': Operation is not implemented or not supported/enabled. - 'internal': Internal errors. Means some invariants expected by underlying system has been broken. If you see one of these errors, something is very broken. - 'unavailable': The service is currently unavailable. This is most likely a transient condition and may be corrected by retrying with a backoff. - 'data-loss': Unrecoverable data loss or corruption. - 'unauthenticated': The request does not have valid authentication credentials for the operation. |
| [FunctionsErrorCodeCore](./functions.md#functionserrorcodecore) | Functions error code string appended after "functions/" product prefix. See [FunctionsErrorCode](./functions.md#functionserrorcode) for full documentation of codes. |
| [HttpsCallable](./functions.md#httpscallable) | A reference to a "callable" HTTP trigger in Google Cloud Functions. |

## function(app, ...)

Expand Down Expand Up @@ -101,7 +103,7 @@ Returns a reference to the callable HTTPS trigger with the given name.
<b>Signature:</b>

```typescript
export declare function httpsCallable<RequestData = unknown, ResponseData = unknown>(functionsInstance: Functions, name: string, options?: HttpsCallableOptions): HttpsCallable<RequestData, ResponseData>;
export declare function httpsCallable<RequestData = unknown, ResponseData = unknown, StreamData = unknown>(functionsInstance: Functions, name: string, options?: HttpsCallableOptions): HttpsCallable<RequestData, ResponseData, StreamData>;
```

#### Parameters
Expand All @@ -114,7 +116,7 @@ export declare function httpsCallable<RequestData = unknown, ResponseData = unkn

<b>Returns:</b>

[HttpsCallable](./functions.md#httpscallable)<!-- -->&lt;RequestData, ResponseData&gt;
[HttpsCallable](./functions.httpscallable.md#httpscallable_interface)<!-- -->&lt;RequestData, ResponseData, StreamData&gt;

### httpsCallableFromURL(functionsInstance, url, options) {:#httpscallablefromurl_7af6987}

Expand All @@ -123,7 +125,7 @@ Returns a reference to the callable HTTPS trigger with the specified url.
<b>Signature:</b>

```typescript
export declare function httpsCallableFromURL<RequestData = unknown, ResponseData = unknown>(functionsInstance: Functions, url: string, options?: HttpsCallableOptions): HttpsCallable<RequestData, ResponseData>;
export declare function httpsCallableFromURL<RequestData = unknown, ResponseData = unknown, StreamData = unknown>(functionsInstance: Functions, url: string, options?: HttpsCallableOptions): HttpsCallable<RequestData, ResponseData, StreamData>;
```

#### Parameters
Expand All @@ -136,7 +138,7 @@ export declare function httpsCallableFromURL<RequestData = unknown, ResponseData

<b>Returns:</b>

[HttpsCallable](./functions.md#httpscallable)<!-- -->&lt;RequestData, ResponseData&gt;
[HttpsCallable](./functions.httpscallable.md#httpscallable_interface)<!-- -->&lt;RequestData, ResponseData, StreamData&gt;

## FunctionsErrorCode

Expand All @@ -159,13 +161,3 @@ Functions error code string appended after "functions/" product prefix. See [Fun
```typescript
export type FunctionsErrorCodeCore = 'ok' | 'cancelled' | 'unknown' | 'invalid-argument' | 'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' | 'data-loss' | 'unauthenticated';
```

## HttpsCallable

A reference to a "callable" HTTP trigger in Google Cloud Functions.

<b>Signature:</b>

```typescript
export type HttpsCallable<RequestData = unknown, ResponseData = unknown> = (data?: RequestData | null) => Promise<HttpsCallableResult<ResponseData>>;
```
2 changes: 1 addition & 1 deletion packages/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"test:browser": "karma start",
"test:browser:debug": "karma start --browsers=Chrome --auto-watch",
"test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'src/{,!(browser)/**/}*.test.ts' --file src/index.ts --config ../../config/mocharc.node.js",
"test:emulator": "env FIREBASE_FUNCTIONS_EMULATOR_ORIGIN=http://localhost:5005 run-p --npm-path npm test:node",
"test:emulator": "env FIREBASE_FUNCTIONS_EMULATOR_ORIGIN=http://127.0.0.1:5005 run-p --npm-path npm test:node",
"trusted-type-check": "tsec -p tsconfig.json --noEmit",
"api-report": "api-extractor run --local --verbose",
"doc": "api-documenter markdown --input temp --output docs",
Expand Down
17 changes: 11 additions & 6 deletions packages/functions/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,16 @@ export function connectFunctionsEmulator(
* @param name - The name of the trigger.
* @public
*/
export function httpsCallable<RequestData = unknown, ResponseData = unknown>(
export function httpsCallable<
RequestData = unknown,
ResponseData = unknown,
StreamData = unknown
>(
functionsInstance: Functions,
name: string,
options?: HttpsCallableOptions
): HttpsCallable<RequestData, ResponseData> {
return _httpsCallable<RequestData, ResponseData>(
): HttpsCallable<RequestData, ResponseData, StreamData> {
return _httpsCallable<RequestData, ResponseData, StreamData>(
getModularInstance<FunctionsService>(functionsInstance as FunctionsService),
name,
options
Expand All @@ -107,13 +111,14 @@ export function httpsCallable<RequestData = unknown, ResponseData = unknown>(
*/
export function httpsCallableFromURL<
RequestData = unknown,
ResponseData = unknown
ResponseData = unknown,
StreamData = unknown
>(
functionsInstance: Functions,
url: string,
options?: HttpsCallableOptions
): HttpsCallable<RequestData, ResponseData> {
return _httpsCallableFromURL<RequestData, ResponseData>(
): HttpsCallable<RequestData, ResponseData, StreamData> {
return _httpsCallableFromURL<RequestData, ResponseData, StreamData>(
getModularInstance<FunctionsService>(functionsInstance as FunctionsService),
url,
options
Expand Down
Loading

0 comments on commit f05509e

Please sign in to comment.