diff --git a/.changeset/brave-boats-arrive.md b/.changeset/brave-boats-arrive.md deleted file mode 100644 index 1e2f703ddb3..00000000000 --- a/.changeset/brave-boats-arrive.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@firebase/remote-config': patch -'@firebase/analytics': patch -'firebase': patch ---- - -Add rollup config to generate modular typings for google3 diff --git a/.changeset/brown-pens-confess.md b/.changeset/brown-pens-confess.md deleted file mode 100644 index 038b177796e..00000000000 --- a/.changeset/brown-pens-confess.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@firebase/storage": patch -"@firebase/util": patch ---- - -Fixed issue where Storage on Firebase Studio throws CORS errors. diff --git a/.changeset/giant-lamps-live.md b/.changeset/giant-lamps-live.md deleted file mode 100644 index f66c22deb86..00000000000 --- a/.changeset/giant-lamps-live.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@firebase/app': patch ---- - -Add "react-native" entry point to @firebase/app diff --git a/.changeset/hungry-tables-exercise.md b/.changeset/hungry-tables-exercise.md new file mode 100644 index 00000000000..81f326b4f4a --- /dev/null +++ b/.changeset/hungry-tables-exercise.md @@ -0,0 +1,6 @@ +--- +'firebase': patch +'@firebase/ai': patch +--- + +Imagen Generation is now Generally Available (GA). diff --git a/.changeset/lazy-donuts-agree.md b/.changeset/lazy-donuts-agree.md new file mode 100644 index 00000000000..f2baca4dcab --- /dev/null +++ b/.changeset/lazy-donuts-agree.md @@ -0,0 +1,9 @@ +--- +'firebase': minor +'@firebase/ai': minor +--- + +Added a `sendFunctionResponses` method to `LiveSession`, allowing function responses to be sent during realtime sessions. +Fixed an issue where function responses during audio conversations caused the WebSocket connection to close. See [GitHub Issue #9264](https://github.com/firebase/firebase-js-sdk/issues/9264). + - **Breaking Change**: Changed the `functionCallingHandler` property in `StartAudioConversationOptions` so that it now must return a `Promise`. + This breaking change is allowed in a minor release since the Live API is in Public Preview. diff --git a/.changeset/long-pets-sell.md b/.changeset/long-pets-sell.md deleted file mode 100644 index d340f7da82c..00000000000 --- a/.changeset/long-pets-sell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@firebase/firestore': patch ---- - -Internal listener registration change for IndexedDB "versionchange" events. diff --git a/.changeset/loud-tigers-compare.md b/.changeset/loud-tigers-compare.md new file mode 100644 index 00000000000..cd4e5412311 --- /dev/null +++ b/.changeset/loud-tigers-compare.md @@ -0,0 +1,6 @@ +--- +'firebase': patch +'@firebase/ai': patch +--- + +The Gemini Developer API is now Generally Available (GA). diff --git a/.changeset/moody-comics-speak.md b/.changeset/moody-comics-speak.md deleted file mode 100644 index 9a178a6605b..00000000000 --- a/.changeset/moody-comics-speak.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@firebase/firestore': minor -'firebase': minor ---- - -Added support for Firestore result types to be serialized with `toJSON` and then deserialized with `fromJSON` methods on the objects. - -Addeed support to resume `onSnapshot` listeners in the CSR phase based on serialized `DataSnapshot`s and `QuerySnapshot`s built in the SSR phase. diff --git a/.changeset/old-candles-confess.md b/.changeset/old-candles-confess.md deleted file mode 100644 index 6fbe742818f..00000000000 --- a/.changeset/old-candles-confess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@firebase/ai': patch ---- - -Add deprecation label to `totalBillableCharacters`. `totalTokens` should be used instead. diff --git a/.changeset/poor-cobras-dream.md b/.changeset/poor-cobras-dream.md new file mode 100644 index 00000000000..3ff9e7bc7bd --- /dev/null +++ b/.changeset/poor-cobras-dream.md @@ -0,0 +1,5 @@ +--- +'@firebase/ai': patch +--- + +Updated SDK to handle empty parts when streaming. diff --git a/.changeset/poor-rings-admire.md b/.changeset/poor-rings-admire.md new file mode 100644 index 00000000000..1b63c0138d0 --- /dev/null +++ b/.changeset/poor-rings-admire.md @@ -0,0 +1,6 @@ +--- +'firebase': minor +'@firebase/ai': minor +--- + +Added support for the URL context tool, which allows the model to access content from provided public web URLs to inform and enhance its responses. diff --git a/.changeset/spotty-ghosts-kneel.md b/.changeset/spotty-ghosts-kneel.md deleted file mode 100644 index 0db91b7bf19..00000000000 --- a/.changeset/spotty-ghosts-kneel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@firebase/firestore": patch ---- - -Clean up leaked WebChannel instances when the Firestore instance is terminated. diff --git a/.changeset/tender-meals-clap.md b/.changeset/tender-meals-clap.md new file mode 100644 index 00000000000..44509f4f489 --- /dev/null +++ b/.changeset/tender-meals-clap.md @@ -0,0 +1,5 @@ +--- +'@firebase/ai': patch +--- + +Tag code execution with beta tag (public preview). diff --git a/.changeset/tricky-years-pump.md b/.changeset/tricky-years-pump.md deleted file mode 100644 index 94bf68604cc..00000000000 --- a/.changeset/tricky-years-pump.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'firebase': minor -'@firebase/ai': minor ---- - -Add `title`, `maximum`, `minimum`, `propertyOrdering` to Schema builder diff --git a/.changeset/young-timers-jump.md b/.changeset/young-timers-jump.md new file mode 100644 index 00000000000..2c572813ae8 --- /dev/null +++ b/.changeset/young-timers-jump.md @@ -0,0 +1,6 @@ +--- +'@firebase/analytics-interop-types': patch +'@firebase/analytics': patch +--- + +Expose `setUserProperties` on internal Analytics instance. diff --git a/.github/workflows/canary-deploy.yml b/.github/workflows/canary-deploy.yml index 4b4cce63761..0d93ceefad8 100644 --- a/.github/workflows/canary-deploy.yml +++ b/.github/workflows/canary-deploy.yml @@ -31,7 +31,7 @@ jobs: with: # Canary release script requires git history and tags. fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml index b3df2555c76..26eb962887f 100644 --- a/.github/workflows/check-changeset.yml +++ b/.github/workflows/check-changeset.yml @@ -37,7 +37,7 @@ jobs: with: # This makes Actions fetch all Git history so check_changeset script can diff properly. fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml index 4afd97a131f..2e57efd0adf 100644 --- a/.github/workflows/check-docs.yml +++ b/.github/workflows/check-docs.yml @@ -27,7 +27,7 @@ jobs: with: # get all history for the diff fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/check-pkg-paths.yml b/.github/workflows/check-pkg-paths.yml index 96dfc6f6556..c7ae3c0c133 100644 --- a/.github/workflows/check-pkg-paths.yml +++ b/.github/workflows/check-pkg-paths.yml @@ -27,7 +27,7 @@ jobs: with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/deploy-config.yml b/.github/workflows/deploy-config.yml index 0c3604e4704..c6e32689e1d 100644 --- a/.github/workflows/deploy-config.yml +++ b/.github/workflows/deploy-config.yml @@ -35,7 +35,7 @@ jobs: with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up node (20) + - name: Set up node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index fbf43beada1..f9ac06ab9c0 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout Repo uses: actions/checkout@v4 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@master with: node-version: 22.10.0 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f0bbd672fc3..ef232d9ddf5 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -31,7 +31,7 @@ jobs: with: # get all history for the diff fetch-depth: 0 - - name: Set up node (20) + - name: Set up node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3ae2ae0a074..c922f9c6a67 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up node (20) + - name: Set up node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/prerelease-manual-deploy.yml b/.github/workflows/prerelease-manual-deploy.yml index 73e82f11943..cf85836d997 100644 --- a/.github/workflows/prerelease-manual-deploy.yml +++ b/.github/workflows/prerelease-manual-deploy.yml @@ -34,7 +34,7 @@ jobs: with: # Canary release script requires git history and tags. fetch-depth: 0 - - name: Set up node (20) + - name: Set up node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/release-prod.yml b/.github/workflows/release-prod.yml index c89c7934db6..253ae95120f 100644 --- a/.github/workflows/release-prod.yml +++ b/.github/workflows/release-prod.yml @@ -32,7 +32,7 @@ jobs: contents: write steps: - - name: Set up node (20) + - name: Set up node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index e75ee4e703d..52aafa9273f 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -47,7 +47,7 @@ jobs: # Block this workflow if run on a non-release branch. if: github.event.inputs.release-branch == 'release' || endsWith(github.event.inputs.release-branch, '-releasebranch') steps: - - name: Set up node (20) + - name: Set up node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index dd74d2437e4..02c3ab0326f 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -40,7 +40,7 @@ jobs: run: | npx @puppeteer/browsers install chrome@stable - uses: actions/checkout@v4 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -79,7 +79,7 @@ jobs: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -126,7 +126,7 @@ jobs: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -166,7 +166,7 @@ jobs: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -209,7 +209,7 @@ jobs: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/test-changed-auth.yml b/.github/workflows/test-changed-auth.yml index b72c7cd9e2d..2ae77916492 100644 --- a/.github/workflows/test-changed-auth.yml +++ b/.github/workflows/test-changed-auth.yml @@ -56,7 +56,7 @@ jobs: with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -81,7 +81,7 @@ jobs: with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -105,7 +105,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/test-changed-fcm-integration.yml b/.github/workflows/test-changed-fcm-integration.yml index ff6023274a4..2fb6f9b890b 100644 --- a/.github/workflows/test-changed-fcm-integration.yml +++ b/.github/workflows/test-changed-fcm-integration.yml @@ -38,7 +38,7 @@ jobs: with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/test-changed-firestore-integration.yml b/.github/workflows/test-changed-firestore-integration.yml index 6841bdd47d6..b894e70e1ec 100644 --- a/.github/workflows/test-changed-firestore-integration.yml +++ b/.github/workflows/test-changed-firestore-integration.yml @@ -70,7 +70,7 @@ jobs: rm -f "$output_file" continue-on-error: true - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/test-changed-firestore.yml b/.github/workflows/test-changed-firestore.yml index 46d36059d14..feb5eb0f672 100644 --- a/.github/workflows/test-changed-firestore.yml +++ b/.github/workflows/test-changed-firestore.yml @@ -37,7 +37,7 @@ jobs: with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -84,7 +84,7 @@ jobs: needs: build if: ${{ needs.build.outputs.changed == 'true'}} steps: - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -112,7 +112,7 @@ jobs: needs: build if: ${{ needs.build.outputs.changed == 'true'}} steps: - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -142,7 +142,7 @@ jobs: needs: build if: ${{ github.event_name != 'pull_request' }} steps: - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -175,7 +175,7 @@ jobs: steps: - name: install Firefox stable run: npx @puppeteer/browsers install firefox@stable - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -209,7 +209,7 @@ jobs: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -227,7 +227,7 @@ jobs: needs: build if: ${{ needs.build.outputs.changed == 'true'}} steps: - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -264,7 +264,7 @@ jobs: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/test-changed-misc.yml b/.github/workflows/test-changed-misc.yml index ebcb2d1d366..52abe5ddddb 100644 --- a/.github/workflows/test-changed-misc.yml +++ b/.github/workflows/test-changed-misc.yml @@ -31,7 +31,7 @@ jobs: with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/.github/workflows/test-changed.yml b/.github/workflows/test-changed.yml index 948267aa9e7..5a0f18600f0 100644 --- a/.github/workflows/test-changed.yml +++ b/.github/workflows/test-changed.yml @@ -31,7 +31,7 @@ jobs: with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -57,7 +57,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 @@ -84,7 +84,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Node (20) + - name: Set up Node (22) uses: actions/setup-node@v4 with: node-version: 22.10.0 diff --git a/common/api-review/ai.api.md b/common/api-review/ai.api.md index ab79447798f..debea0a8549 100644 --- a/common/api-review/ai.api.md +++ b/common/api-review/ai.api.md @@ -15,6 +15,7 @@ export interface AI { backend: Backend; // @deprecated (undocumented) location: string; + options?: AIOptions; } // @public @@ -27,25 +28,25 @@ export class AIError extends FirebaseError { } // @public -const enum AIErrorCode { - API_NOT_ENABLED = "api-not-enabled", - ERROR = "error", - FETCH_ERROR = "fetch-error", - INVALID_CONTENT = "invalid-content", - INVALID_SCHEMA = "invalid-schema", - NO_API_KEY = "no-api-key", - NO_APP_ID = "no-app-id", - NO_MODEL = "no-model", - NO_PROJECT_ID = "no-project-id", - PARSE_FAILED = "parse-failed", - REQUEST_ERROR = "request-error", - RESPONSE_ERROR = "response-error", - UNSUPPORTED = "unsupported" -} - -export { AIErrorCode } +export const AIErrorCode: { + readonly ERROR: "error"; + readonly REQUEST_ERROR: "request-error"; + readonly RESPONSE_ERROR: "response-error"; + readonly FETCH_ERROR: "fetch-error"; + readonly SESSION_CLOSED: "session-closed"; + readonly INVALID_CONTENT: "invalid-content"; + readonly API_NOT_ENABLED: "api-not-enabled"; + readonly INVALID_SCHEMA: "invalid-schema"; + readonly NO_API_KEY: "no-api-key"; + readonly NO_APP_ID: "no-app-id"; + readonly NO_MODEL: "no-model"; + readonly NO_PROJECT_ID: "no-project-id"; + readonly PARSE_FAILED: "parse-failed"; + readonly UNSUPPORTED: "unsupported"; +}; -export { AIErrorCode as VertexAIErrorCode } +// @public +export type AIErrorCode = (typeof AIErrorCode)[keyof typeof AIErrorCode]; // @public export abstract class AIModel { @@ -54,7 +55,7 @@ export abstract class AIModel { // Warning: (ae-forgotten-export) The symbol "ApiSettings" needs to be exported by the entry point index.d.ts // // @internal (undocumented) - protected _apiSettings: ApiSettings; + _apiSettings: ApiSettings; readonly model: string; // @internal static normalizeModelName(modelName: string, backendType: BackendType): string; @@ -62,7 +63,19 @@ export abstract class AIModel { // @public export interface AIOptions { - backend: Backend; + backend?: Backend; + useLimitedUseAppCheckTokens?: boolean; +} + +// @public +export class AnyOfSchema extends Schema { + constructor(schemaParams: SchemaParams & { + anyOf: TypedSchema[]; + }); + // (undocumented) + anyOf: TypedSchema[]; + // @internal (undocumented) + toJSON(): SchemaRequest; } // @public @@ -74,6 +87,11 @@ export class ArraySchema extends Schema { toJSON(): SchemaRequest; } +// @beta +export interface AudioConversationController { + stop: () => Promise; +} + // @public export abstract class Backend { protected constructor(type: BackendType); @@ -98,12 +116,15 @@ export interface BaseParams { } // @public -export enum BlockReason { - BLOCKLIST = "BLOCKLIST", - OTHER = "OTHER", - PROHIBITED_CONTENT = "PROHIBITED_CONTENT", - SAFETY = "SAFETY" -} +export const BlockReason: { + readonly SAFETY: "SAFETY"; + readonly OTHER: "OTHER"; + readonly BLOCKLIST: "BLOCKLIST"; + readonly PROHIBITED_CONTENT: "PROHIBITED_CONTENT"; +}; + +// @public +export type BlockReason = (typeof BlockReason)[keyof typeof BlockReason]; // @public export class BooleanSchema extends Schema { @@ -112,7 +133,8 @@ export class BooleanSchema extends Schema { // @public export class ChatSession { - constructor(apiSettings: ApiSettings, model: string, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined); + // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "ChromeAdapter" which is marked as @beta + constructor(apiSettings: ApiSettings, model: string, chromeAdapter?: ChromeAdapter | undefined, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined); getHistory(): Promise; // (undocumented) model: string; @@ -124,6 +146,15 @@ export class ChatSession { sendMessageStream(request: string | Array): Promise; } +// @beta +export interface ChromeAdapter { + // @internal (undocumented) + countTokens(request: CountTokensRequest): Promise; + generateContent(request: GenerateContentRequest): Promise; + generateContentStream(request: GenerateContentRequest): Promise; + isAvailable(request: GenerateContentRequest): Promise; +} + // @public export interface Citation { // (undocumented) @@ -144,6 +175,39 @@ export interface CitationMetadata { citations: Citation[]; } +// @beta +export interface CodeExecutionResult { + outcome?: Outcome; + output?: string; +} + +// @beta +export interface CodeExecutionResultPart { + // (undocumented) + codeExecutionResult?: CodeExecutionResult; + // (undocumented) + executableCode?: never; + // (undocumented) + fileData: never; + // (undocumented) + functionCall?: never; + // (undocumented) + functionResponse?: never; + // (undocumented) + inlineData?: never; + // (undocumented) + text?: never; + // (undocumented) + thought?: never; + // @internal (undocumented) + thoughtSignature?: never; +} + +// @beta +export interface CodeExecutionTool { + codeExecution: {}; +} + // @public export interface Content { // (undocumented) @@ -191,10 +255,10 @@ export { Date_2 as Date } // @public export interface EnhancedGenerateContentResponse extends GenerateContentResponse { - // (undocumented) functionCalls: () => FunctionCall[] | undefined; inlineDataParts: () => InlineDataPart[] | undefined; text: () => string; + thoughtSummary: () => string | undefined; } // @public @@ -207,6 +271,34 @@ export interface ErrorDetails { reason?: string; } +// @beta +export interface ExecutableCode { + code?: string; + language?: Language; +} + +// @beta +export interface ExecutableCodePart { + // (undocumented) + codeExecutionResult?: never; + // (undocumented) + executableCode?: ExecutableCode; + // (undocumented) + fileData: never; + // (undocumented) + functionCall?: never; + // (undocumented) + functionResponse?: never; + // (undocumented) + inlineData?: never; + // (undocumented) + text?: never; + // (undocumented) + thought?: never; + // @internal (undocumented) + thoughtSignature?: never; +} + // @public export interface FileData { // (undocumented) @@ -217,6 +309,10 @@ export interface FileData { // @public export interface FileDataPart { + // (undocumented) + codeExecutionResult?: never; + // (undocumented) + executableCode?: never; // (undocumented) fileData: FileData; // (undocumented) @@ -227,25 +323,33 @@ export interface FileDataPart { inlineData?: never; // (undocumented) text?: never; + // (undocumented) + thought?: boolean; + // @internal (undocumented) + thoughtSignature?: never; } // @public -export enum FinishReason { - BLOCKLIST = "BLOCKLIST", - MALFORMED_FUNCTION_CALL = "MALFORMED_FUNCTION_CALL", - MAX_TOKENS = "MAX_TOKENS", - OTHER = "OTHER", - PROHIBITED_CONTENT = "PROHIBITED_CONTENT", - RECITATION = "RECITATION", - SAFETY = "SAFETY", - SPII = "SPII", - STOP = "STOP" -} +export const FinishReason: { + readonly STOP: "STOP"; + readonly MAX_TOKENS: "MAX_TOKENS"; + readonly SAFETY: "SAFETY"; + readonly RECITATION: "RECITATION"; + readonly OTHER: "OTHER"; + readonly BLOCKLIST: "BLOCKLIST"; + readonly PROHIBITED_CONTENT: "PROHIBITED_CONTENT"; + readonly SPII: "SPII"; + readonly MALFORMED_FUNCTION_CALL: "MALFORMED_FUNCTION_CALL"; +}; + +// @public +export type FinishReason = (typeof FinishReason)[keyof typeof FinishReason]; // @public export interface FunctionCall { // (undocumented) args: object; + id?: string; // (undocumented) name: string; } @@ -259,14 +363,21 @@ export interface FunctionCallingConfig { } // @public (undocumented) -export enum FunctionCallingMode { - ANY = "ANY", - AUTO = "AUTO", - NONE = "NONE" -} +export const FunctionCallingMode: { + readonly AUTO: "AUTO"; + readonly ANY: "ANY"; + readonly NONE: "NONE"; +}; + +// @public (undocumented) +export type FunctionCallingMode = (typeof FunctionCallingMode)[keyof typeof FunctionCallingMode]; // @public export interface FunctionCallPart { + // (undocumented) + codeExecutionResult?: never; + // (undocumented) + executableCode?: never; // (undocumented) functionCall: FunctionCall; // (undocumented) @@ -275,13 +386,17 @@ export interface FunctionCallPart { inlineData?: never; // (undocumented) text?: never; + // (undocumented) + thought?: boolean; + // @internal (undocumented) + thoughtSignature?: never; } // @public export interface FunctionDeclaration { description: string; name: string; - parameters?: ObjectSchemaInterface; + parameters?: ObjectSchema | ObjectSchemaRequest; } // @public @@ -291,6 +406,7 @@ export interface FunctionDeclarationsTool { // @public export interface FunctionResponse { + id?: string; // (undocumented) name: string; // (undocumented) @@ -299,6 +415,10 @@ export interface FunctionResponse { // @public export interface FunctionResponsePart { + // (undocumented) + codeExecutionResult?: never; + // (undocumented) + executableCode?: never; // (undocumented) functionCall?: never; // (undocumented) @@ -307,6 +427,10 @@ export interface FunctionResponsePart { inlineData?: never; // (undocumented) text?: never; + // (undocumented) + thought?: boolean; + // @internal (undocumented) + thoughtSignature?: never; } // @public @@ -325,6 +449,10 @@ export interface GenerateContentCandidate { index: number; // (undocumented) safetyRatings?: SafetyRating[]; + // Warning: (ae-incompatible-release-tags) The symbol "urlContextMetadata" is marked as @public, but its signature references "URLContextMetadata" which is marked as @beta + // + // (undocumented) + urlContextMetadata?: URLContextMetadata; } // @public @@ -381,6 +509,7 @@ export interface GenerationConfig { stopSequences?: string[]; // (undocumented) temperature?: number; + thinkingConfig?: ThinkingConfig; // (undocumented) topK?: number; // (undocumented) @@ -396,7 +525,8 @@ export interface GenerativeContentBlob { // @public export class GenerativeModel extends AIModel { - constructor(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions); + // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "ChromeAdapter" which is marked as @beta + constructor(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions, chromeAdapter?: ChromeAdapter | undefined); countTokens(request: CountTokensRequest | string | Array): Promise; generateContent(request: GenerateContentRequest | string | Array): Promise; generateContentStream(request: GenerateContentRequest | string | Array): Promise; @@ -418,14 +548,16 @@ export class GenerativeModel extends AIModel { // @public export function getAI(app?: FirebaseApp, options?: AIOptions): AI; +// Warning: (ae-incompatible-release-tags) The symbol "getGenerativeModel" is marked as @public, but its signature references "HybridParams" which is marked as @beta +// // @public -export function getGenerativeModel(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions): GenerativeModel; +export function getGenerativeModel(ai: AI, modelParams: ModelParams | HybridParams, requestOptions?: RequestOptions): GenerativeModel; -// @beta +// @public export function getImagenModel(ai: AI, modelParams: ImagenModelParams, requestOptions?: RequestOptions): ImagenModel; -// @public @deprecated (undocumented) -export function getVertexAI(app?: FirebaseApp, options?: VertexAIOptions): VertexAI; +// @beta +export function getLiveGenerativeModel(ai: AI, modelParams: LiveModelParams): LiveGenerativeModel; // @public export class GoogleAIBackend extends Backend { @@ -472,6 +604,8 @@ export interface GoogleAIGenerateContentCandidate { index: number; // (undocumented) safetyRatings?: SafetyRating[]; + // (undocumented) + urlContextMetadata?: URLContextMetadata; } // Warning: (ae-internal-missing-underscore) The name "GoogleAIGenerateContentResponse" should be prefixed with an underscore because the declaration is marked as @internal @@ -486,88 +620,117 @@ export interface GoogleAIGenerateContentResponse { usageMetadata?: UsageMetadata; } -// @public @deprecated (undocumented) -export interface GroundingAttribution { - // (undocumented) - confidenceScore?: number; - // (undocumented) - retrievedContext?: RetrievedContextAttribution; - // (undocumented) - segment: Segment; - // (undocumented) - web?: WebAttribution; +// @public +export interface GoogleSearch { +} + +// @public +export interface GoogleSearchTool { + googleSearch: GoogleSearch; +} + +// @public +export interface GroundingChunk { + web?: WebGroundingChunk; } // @public export interface GroundingMetadata { + groundingChunks?: GroundingChunk[]; + groundingSupports?: GroundingSupport[]; // @deprecated (undocumented) - groundingAttributions: GroundingAttribution[]; - // (undocumented) retrievalQueries?: string[]; - // (undocumented) + searchEntryPoint?: SearchEntrypoint; webSearchQueries?: string[]; } // @public -export enum HarmBlockMethod { - PROBABILITY = "PROBABILITY", - SEVERITY = "SEVERITY" +export interface GroundingSupport { + groundingChunkIndices?: number[]; + segment?: Segment; } // @public -export enum HarmBlockThreshold { - BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE", - BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE", - BLOCK_NONE = "BLOCK_NONE", - BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH", - OFF = "OFF" -} +export const HarmBlockMethod: { + readonly SEVERITY: "SEVERITY"; + readonly PROBABILITY: "PROBABILITY"; +}; // @public -export enum HarmCategory { - // (undocumented) - HARM_CATEGORY_DANGEROUS_CONTENT = "HARM_CATEGORY_DANGEROUS_CONTENT", - // (undocumented) - HARM_CATEGORY_HARASSMENT = "HARM_CATEGORY_HARASSMENT", - // (undocumented) - HARM_CATEGORY_HATE_SPEECH = "HARM_CATEGORY_HATE_SPEECH", - // (undocumented) - HARM_CATEGORY_SEXUALLY_EXPLICIT = "HARM_CATEGORY_SEXUALLY_EXPLICIT" -} +export type HarmBlockMethod = (typeof HarmBlockMethod)[keyof typeof HarmBlockMethod]; // @public -export enum HarmProbability { - HIGH = "HIGH", - LOW = "LOW", - MEDIUM = "MEDIUM", - NEGLIGIBLE = "NEGLIGIBLE" -} +export const HarmBlockThreshold: { + readonly BLOCK_LOW_AND_ABOVE: "BLOCK_LOW_AND_ABOVE"; + readonly BLOCK_MEDIUM_AND_ABOVE: "BLOCK_MEDIUM_AND_ABOVE"; + readonly BLOCK_ONLY_HIGH: "BLOCK_ONLY_HIGH"; + readonly BLOCK_NONE: "BLOCK_NONE"; + readonly OFF: "OFF"; +}; // @public -export enum HarmSeverity { - HARM_SEVERITY_HIGH = "HARM_SEVERITY_HIGH", - HARM_SEVERITY_LOW = "HARM_SEVERITY_LOW", - HARM_SEVERITY_MEDIUM = "HARM_SEVERITY_MEDIUM", - HARM_SEVERITY_NEGLIGIBLE = "HARM_SEVERITY_NEGLIGIBLE", - HARM_SEVERITY_UNSUPPORTED = "HARM_SEVERITY_UNSUPPORTED" -} +export type HarmBlockThreshold = (typeof HarmBlockThreshold)[keyof typeof HarmBlockThreshold]; + +// @public +export const HarmCategory: { + readonly HARM_CATEGORY_HATE_SPEECH: "HARM_CATEGORY_HATE_SPEECH"; + readonly HARM_CATEGORY_SEXUALLY_EXPLICIT: "HARM_CATEGORY_SEXUALLY_EXPLICIT"; + readonly HARM_CATEGORY_HARASSMENT: "HARM_CATEGORY_HARASSMENT"; + readonly HARM_CATEGORY_DANGEROUS_CONTENT: "HARM_CATEGORY_DANGEROUS_CONTENT"; +}; + +// @public +export type HarmCategory = (typeof HarmCategory)[keyof typeof HarmCategory]; + +// @public +export const HarmProbability: { + readonly NEGLIGIBLE: "NEGLIGIBLE"; + readonly LOW: "LOW"; + readonly MEDIUM: "MEDIUM"; + readonly HIGH: "HIGH"; +}; + +// @public +export type HarmProbability = (typeof HarmProbability)[keyof typeof HarmProbability]; + +// @public +export const HarmSeverity: { + readonly HARM_SEVERITY_NEGLIGIBLE: "HARM_SEVERITY_NEGLIGIBLE"; + readonly HARM_SEVERITY_LOW: "HARM_SEVERITY_LOW"; + readonly HARM_SEVERITY_MEDIUM: "HARM_SEVERITY_MEDIUM"; + readonly HARM_SEVERITY_HIGH: "HARM_SEVERITY_HIGH"; + readonly HARM_SEVERITY_UNSUPPORTED: "HARM_SEVERITY_UNSUPPORTED"; +}; + +// @public +export type HarmSeverity = (typeof HarmSeverity)[keyof typeof HarmSeverity]; // @beta -export enum ImagenAspectRatio { - LANDSCAPE_16x9 = "16:9", - LANDSCAPE_3x4 = "3:4", - PORTRAIT_4x3 = "4:3", - PORTRAIT_9x16 = "9:16", - SQUARE = "1:1" +export interface HybridParams { + inCloudParams?: ModelParams; + mode: InferenceMode; + onDeviceParams?: OnDeviceParams; } +// @public +export const ImagenAspectRatio: { + readonly SQUARE: "1:1"; + readonly LANDSCAPE_3x4: "3:4"; + readonly PORTRAIT_4x3: "4:3"; + readonly LANDSCAPE_16x9: "16:9"; + readonly PORTRAIT_9x16: "9:16"; +}; + +// @public +export type ImagenAspectRatio = (typeof ImagenAspectRatio)[keyof typeof ImagenAspectRatio]; + // @public export interface ImagenGCSImage { gcsURI: string; mimeType: string; } -// @beta +// @public export interface ImagenGenerationConfig { addWatermark?: boolean; aspectRatio?: ImagenAspectRatio; @@ -576,13 +739,13 @@ export interface ImagenGenerationConfig { numberOfImages?: number; } -// @beta +// @public export interface ImagenGenerationResponse { filteredReason?: string; images: T[]; } -// @beta +// @public export class ImagenImageFormat { compressionQuality?: number; static jpeg(compressionQuality?: number): ImagenImageFormat; @@ -590,13 +753,13 @@ export class ImagenImageFormat { static png(): ImagenImageFormat; } -// @beta +// @public export interface ImagenInlineImage { bytesBase64Encoded: string; mimeType: string; } -// @beta +// @public export class ImagenModel extends AIModel { constructor(ai: AI, modelParams: ImagenModelParams, requestOptions?: RequestOptions | undefined); generateImages(prompt: string): Promise>; @@ -608,36 +771,57 @@ export class ImagenModel extends AIModel { safetySettings?: ImagenSafetySettings; } -// @beta +// @public export interface ImagenModelParams { generationConfig?: ImagenGenerationConfig; model: string; safetySettings?: ImagenSafetySettings; } -// @beta -export enum ImagenPersonFilterLevel { - ALLOW_ADULT = "allow_adult", - ALLOW_ALL = "allow_all", - BLOCK_ALL = "dont_allow" -} +// @public +export const ImagenPersonFilterLevel: { + readonly BLOCK_ALL: "dont_allow"; + readonly ALLOW_ADULT: "allow_adult"; + readonly ALLOW_ALL: "allow_all"; +}; -// @beta -export enum ImagenSafetyFilterLevel { - BLOCK_LOW_AND_ABOVE = "block_low_and_above", - BLOCK_MEDIUM_AND_ABOVE = "block_medium_and_above", - BLOCK_NONE = "block_none", - BLOCK_ONLY_HIGH = "block_only_high" -} +// @public +export type ImagenPersonFilterLevel = (typeof ImagenPersonFilterLevel)[keyof typeof ImagenPersonFilterLevel]; -// @beta +// @public +export const ImagenSafetyFilterLevel: { + readonly BLOCK_LOW_AND_ABOVE: "block_low_and_above"; + readonly BLOCK_MEDIUM_AND_ABOVE: "block_medium_and_above"; + readonly BLOCK_ONLY_HIGH: "block_only_high"; + readonly BLOCK_NONE: "block_none"; +}; + +// @public +export type ImagenSafetyFilterLevel = (typeof ImagenSafetyFilterLevel)[keyof typeof ImagenSafetyFilterLevel]; + +// @public export interface ImagenSafetySettings { personFilterLevel?: ImagenPersonFilterLevel; safetyFilterLevel?: ImagenSafetyFilterLevel; } +// @beta +export const InferenceMode: { + readonly PREFER_ON_DEVICE: "prefer_on_device"; + readonly ONLY_ON_DEVICE: "only_on_device"; + readonly ONLY_IN_CLOUD: "only_in_cloud"; + readonly PREFER_IN_CLOUD: "prefer_in_cloud"; +}; + +// @beta +export type InferenceMode = (typeof InferenceMode)[keyof typeof InferenceMode]; + // @public export interface InlineDataPart { + // (undocumented) + codeExecutionResult?: never; + // (undocumented) + executableCode?: never; // (undocumented) functionCall?: never; // (undocumented) @@ -646,6 +830,10 @@ export interface InlineDataPart { inlineData: GenerativeContentBlob; // (undocumented) text?: never; + // (undocumented) + thought?: boolean; + // @internal (undocumented) + thoughtSignature?: never; videoMetadata?: VideoMetadata; } @@ -654,16 +842,176 @@ export class IntegerSchema extends Schema { constructor(schemaParams?: SchemaParams); } -// @public -export enum Modality { - AUDIO = "AUDIO", - DOCUMENT = "DOCUMENT", - IMAGE = "IMAGE", - MODALITY_UNSPECIFIED = "MODALITY_UNSPECIFIED", - TEXT = "TEXT", - VIDEO = "VIDEO" +// @beta +export const Language: { + UNSPECIFIED: string; + PYTHON: string; +}; + +// @beta +export type Language = (typeof Language)[keyof typeof Language]; + +// @beta +export interface LanguageModelCreateCoreOptions { + // (undocumented) + expectedInputs?: LanguageModelExpected[]; + // (undocumented) + temperature?: number; + // (undocumented) + topK?: number; +} + +// @beta +export interface LanguageModelCreateOptions extends LanguageModelCreateCoreOptions { + // (undocumented) + initialPrompts?: LanguageModelMessage[]; + // (undocumented) + signal?: AbortSignal; +} + +// @beta +export interface LanguageModelExpected { + // (undocumented) + languages?: string[]; + // (undocumented) + type: LanguageModelMessageType; +} + +// @beta +export interface LanguageModelMessage { + // (undocumented) + content: LanguageModelMessageContent[]; + // (undocumented) + role: LanguageModelMessageRole; } +// @beta +export interface LanguageModelMessageContent { + // (undocumented) + type: LanguageModelMessageType; + // (undocumented) + value: LanguageModelMessageContentValue; +} + +// @beta +export type LanguageModelMessageContentValue = ImageBitmapSource | AudioBuffer | BufferSource | string; + +// @beta +export type LanguageModelMessageRole = 'system' | 'user' | 'assistant'; + +// @beta +export type LanguageModelMessageType = 'text' | 'image' | 'audio'; + +// @beta +export interface LanguageModelPromptOptions { + // (undocumented) + responseConstraint?: object; +} + +// @beta +export interface LiveGenerationConfig { + frequencyPenalty?: number; + maxOutputTokens?: number; + presencePenalty?: number; + responseModalities?: ResponseModality[]; + speechConfig?: SpeechConfig; + temperature?: number; + topK?: number; + topP?: number; +} + +// @beta +export class LiveGenerativeModel extends AIModel { + // Warning: (ae-forgotten-export) The symbol "WebSocketHandler" needs to be exported by the entry point index.d.ts + // + // @internal + constructor(ai: AI, modelParams: LiveModelParams, + _webSocketHandler: WebSocketHandler); + connect(): Promise; + // (undocumented) + generationConfig: LiveGenerationConfig; + // (undocumented) + systemInstruction?: Content; + // (undocumented) + toolConfig?: ToolConfig; + // (undocumented) + tools?: Tool[]; + } + +// @beta +export interface LiveModelParams { + // (undocumented) + generationConfig?: LiveGenerationConfig; + // (undocumented) + model: string; + // (undocumented) + systemInstruction?: string | Part | Content; + // (undocumented) + toolConfig?: ToolConfig; + // (undocumented) + tools?: Tool[]; +} + +// @beta +export const LiveResponseType: { + SERVER_CONTENT: string; + TOOL_CALL: string; + TOOL_CALL_CANCELLATION: string; +}; + +// @beta +export type LiveResponseType = (typeof LiveResponseType)[keyof typeof LiveResponseType]; + +// @beta +export interface LiveServerContent { + interrupted?: boolean; + modelTurn?: Content; + turnComplete?: boolean; + // (undocumented) + type: 'serverContent'; +} + +// @beta +export interface LiveServerToolCall { + functionCalls: FunctionCall[]; + // (undocumented) + type: 'toolCall'; +} + +// @beta +export interface LiveServerToolCallCancellation { + functionIds: string[]; + // (undocumented) + type: 'toolCallCancellation'; +} + +// @beta +export class LiveSession { + // @internal + constructor(webSocketHandler: WebSocketHandler, serverMessages: AsyncGenerator); + close(): Promise; + inConversation: boolean; + isClosed: boolean; + receive(): AsyncGenerator; + send(request: string | Array, turnComplete?: boolean): Promise; + sendFunctionResponses(functionResponses: FunctionResponse[]): Promise; + sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise; + sendMediaStream(mediaChunkStream: ReadableStream): Promise; + } + +// @public +export const Modality: { + readonly MODALITY_UNSPECIFIED: "MODALITY_UNSPECIFIED"; + readonly TEXT: "TEXT"; + readonly IMAGE: "IMAGE"; + readonly VIDEO: "VIDEO"; + readonly AUDIO: "AUDIO"; + readonly DOCUMENT: "DOCUMENT"; +}; + +// @public +export type Modality = (typeof Modality)[keyof typeof Modality]; + // @public export interface ModalityTokenCount { modality: Modality; @@ -703,19 +1051,45 @@ export class ObjectSchema extends Schema { } // @public -export interface ObjectSchemaInterface extends SchemaInterface { +export interface ObjectSchemaRequest extends SchemaRequest { + optionalProperties?: never; + // (undocumented) + type: 'object'; +} + +// @beta +export interface OnDeviceParams { // (undocumented) - optionalProperties?: string[]; + createOptions?: LanguageModelCreateOptions; // (undocumented) - type: SchemaType.OBJECT; + promptOptions?: LanguageModelPromptOptions; } +// @beta +export const Outcome: { + UNSPECIFIED: string; + OK: string; + FAILED: string; + DEADLINE_EXCEEDED: string; +}; + +// @beta +export type Outcome = (typeof Outcome)[keyof typeof Outcome]; + +// Warning: (ae-incompatible-release-tags) The symbol "Part" is marked as @public, but its signature references "ExecutableCodePart" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "Part" is marked as @public, but its signature references "CodeExecutionResultPart" which is marked as @beta +// // @public -export type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | FileDataPart; +export type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | FileDataPart | ExecutableCodePart | CodeExecutionResultPart; // @public export const POSSIBLE_ROLES: readonly ["user", "model", "function", "system"]; +// @beta +export interface PrebuiltVoiceConfig { + voiceName?: string; +} + // @public export interface PromptFeedback { // (undocumented) @@ -735,6 +1109,7 @@ export interface RequestOptions { export const ResponseModality: { readonly TEXT: "TEXT"; readonly IMAGE: "IMAGE"; + readonly AUDIO: "AUDIO"; }; // @beta @@ -778,6 +1153,10 @@ export abstract class Schema implements SchemaInterface { constructor(schemaParams: SchemaInterface); [key: string]: unknown; // (undocumented) + static anyOf(anyOfParams: SchemaParams & { + anyOf: TypedSchema[]; + }): AnyOfSchema; + // (undocumented) static array(arrayParams: SchemaParams & { items: Schema; }): ArraySchema; @@ -809,12 +1188,12 @@ export abstract class Schema implements SchemaInterface { static string(stringParams?: SchemaParams): StringSchema; // @internal toJSON(): SchemaRequest; - type: SchemaType; + type?: SchemaType; } // @public export interface SchemaInterface extends SchemaShared { - type: SchemaType; + type?: SchemaType; } // @public @@ -824,13 +1203,14 @@ export interface SchemaParams extends SchemaShared { // @public export interface SchemaRequest extends SchemaShared { required?: string[]; - type: SchemaType; + type?: SchemaType; } // @public export interface SchemaShared { // (undocumented) [key: string]: unknown; + anyOf?: T[]; description?: string; enum?: string[]; example?: unknown; @@ -849,23 +1229,42 @@ export interface SchemaShared { } // @public -export enum SchemaType { - ARRAY = "array", - BOOLEAN = "boolean", - INTEGER = "integer", - NUMBER = "number", - OBJECT = "object", - STRING = "string" +export const SchemaType: { + readonly STRING: "string"; + readonly NUMBER: "number"; + readonly INTEGER: "integer"; + readonly BOOLEAN: "boolean"; + readonly ARRAY: "array"; + readonly OBJECT: "object"; +}; + +// @public +export type SchemaType = (typeof SchemaType)[keyof typeof SchemaType]; + +// @public +export interface SearchEntrypoint { + renderedContent?: string; } -// @public (undocumented) +// @public export interface Segment { - // (undocumented) endIndex: number; - // (undocumented) partIndex: number; - // (undocumented) startIndex: number; + text: string; +} + +// @beta +export interface SpeechConfig { + voiceConfig?: VoiceConfig; +} + +// @beta +export function startAudioConversation(liveSession: LiveSession, options?: StartAudioConversationOptions): Promise; + +// @beta +export interface StartAudioConversationOptions { + functionCallingHandler?: (functionCalls: FunctionCall[]) => Promise; } // @public @@ -891,6 +1290,10 @@ export class StringSchema extends Schema { // @public export interface TextPart { + // (undocumented) + codeExecutionResult?: never; + // (undocumented) + executableCode?: never; // (undocumented) functionCall?: never; // (undocumented) @@ -899,10 +1302,23 @@ export interface TextPart { inlineData?: never; // (undocumented) text: string; + // (undocumented) + thought?: boolean; + // @internal (undocumented) + thoughtSignature?: string; } // @public -export type Tool = FunctionDeclarationsTool; +export interface ThinkingConfig { + includeThoughts?: boolean; + thinkingBudget?: number; +} + +// Warning: (ae-incompatible-release-tags) The symbol "Tool" is marked as @public, but its signature references "CodeExecutionTool" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "Tool" is marked as @public, but its signature references "URLContextTool" which is marked as @beta +// +// @public +export type Tool = FunctionDeclarationsTool | GoogleSearchTool | CodeExecutionTool | URLContextTool; // @public export interface ToolConfig { @@ -911,7 +1327,39 @@ export interface ToolConfig { } // @public -export type TypedSchema = IntegerSchema | NumberSchema | StringSchema | BooleanSchema | ObjectSchema | ArraySchema; +export type TypedSchema = IntegerSchema | NumberSchema | StringSchema | BooleanSchema | ObjectSchema | ArraySchema | AnyOfSchema; + +// @beta +export interface URLContext { +} + +// @beta +export interface URLContextMetadata { + urlMetadata: URLMetadata[]; +} + +// @beta +export interface URLContextTool { + urlContext: URLContext; +} + +// @beta +export interface URLMetadata { + retrievedUrl?: string; + urlRetrievalStatus?: URLRetrievalStatus; +} + +// @beta +export const URLRetrievalStatus: { + URL_RETRIEVAL_STATUS_UNSPECIFIED: string; + URL_RETRIEVAL_STATUS_SUCCESS: string; + URL_RETRIEVAL_STATUS_ERROR: string; + URL_RETRIEVAL_STATUS_PAYWALL: string; + URL_RETRIEVAL_STATUS_UNSAFE: string; +}; + +// @beta +export type URLRetrievalStatus = (typeof URLRetrievalStatus)[keyof typeof URLRetrievalStatus]; // @public export interface UsageMetadata { @@ -923,37 +1371,30 @@ export interface UsageMetadata { promptTokenCount: number; // (undocumented) promptTokensDetails?: ModalityTokenCount[]; + thoughtsTokenCount?: number; + toolUsePromptTokenCount?: number; + toolUsePromptTokensDetails?: ModalityTokenCount[]; // (undocumented) totalTokenCount: number; } -// @public @deprecated (undocumented) -export type VertexAI = AI; - // @public export class VertexAIBackend extends Backend { constructor(location?: string); readonly location: string; } -// @public @deprecated (undocumented) -export const VertexAIError: typeof AIError; - -// @public @deprecated (undocumented) -export const VertexAIModel: typeof AIModel; - -// @public -export interface VertexAIOptions { - // (undocumented) - location?: string; -} - // @public export interface VideoMetadata { endOffset: string; startOffset: string; } +// @beta +export interface VoiceConfig { + prebuiltVoiceConfig?: PrebuiltVoiceConfig; +} + // @public (undocumented) export interface WebAttribution { // (undocumented) @@ -962,5 +1403,12 @@ export interface WebAttribution { uri: string; } +// @public +export interface WebGroundingChunk { + domain?: string; + title?: string; + uri?: string; +} + ``` diff --git a/common/api-review/app.api.md b/common/api-review/app.api.md index 4e93f1ae87f..c12089c1520 100644 --- a/common/api-review/app.api.md +++ b/common/api-review/app.api.md @@ -110,14 +110,20 @@ export function initializeApp(options: FirebaseOptions, config?: FirebaseAppSett export function initializeApp(): FirebaseApp; // @public -export function initializeServerApp(options: FirebaseOptions | FirebaseApp, config: FirebaseServerAppSettings): FirebaseServerApp; +export function initializeServerApp(options: FirebaseOptions | FirebaseApp, config?: FirebaseServerAppSettings): FirebaseServerApp; + +// @public +export function initializeServerApp(config?: FirebaseServerAppSettings): FirebaseServerApp; // @internal (undocumented) -export function _isFirebaseApp(obj: FirebaseApp | FirebaseOptions): obj is FirebaseApp; +export function _isFirebaseApp(obj: FirebaseApp | FirebaseOptions | FirebaseAppSettings): obj is FirebaseApp; // @internal (undocumented) export function _isFirebaseServerApp(obj: FirebaseApp | FirebaseServerApp | null | undefined): obj is FirebaseServerApp; +// @internal (undocumented) +export function _isFirebaseServerAppSettings(obj: FirebaseApp | FirebaseOptions | FirebaseAppSettings): obj is FirebaseServerAppSettings; + // @public export function onLog(logCallback: LogCallback | null, options?: LogOptions): void; diff --git a/common/api-review/remote-config.api.md b/common/api-review/remote-config.api.md index 213335929dd..a9f5131e0bf 100644 --- a/common/api-review/remote-config.api.md +++ b/common/api-review/remote-config.api.md @@ -5,10 +5,23 @@ ```ts import { FirebaseApp } from '@firebase/app'; +import { FirebaseError } from '@firebase/app'; // @public export function activate(remoteConfig: RemoteConfig): Promise; +// @public +export interface ConfigUpdate { + getUpdatedKeys(): Set; +} + +// @public +export interface ConfigUpdateObserver { + complete: () => void; + error: (error: FirebaseError) => void; + next: (configUpdate: ConfigUpdate) => void; +} + // @public export interface CustomSignals { // (undocumented) @@ -29,11 +42,15 @@ export interface FetchResponse { config?: FirebaseRemoteConfigObject; eTag?: string; status: number; + templateVersion?: number; } // @public export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle'; +// @public +export type FetchType = 'BASE' | 'REALTIME'; + // @public export interface FirebaseRemoteConfigObject { // (undocumented) @@ -64,6 +81,9 @@ export function isSupported(): Promise; // @public export type LogLevel = 'debug' | 'error' | 'silent'; +// @public +export function onConfigUpdate(remoteConfig: RemoteConfig, observer: ConfigUpdateObserver): Unsubscribe; + // @public export interface RemoteConfig { app: FirebaseApp; @@ -93,6 +113,9 @@ export function setCustomSignals(remoteConfig: RemoteConfig, customSignals: Cust // @public export function setLogLevel(remoteConfig: RemoteConfig, logLevel: LogLevel): void; +// @public +export type Unsubscribe = () => void; + // @public export interface Value { asBoolean(): boolean; diff --git a/common/api-review/vertexai.api.md b/common/api-review/vertexai.api.md deleted file mode 100644 index 42da114f9e9..00000000000 --- a/common/api-review/vertexai.api.md +++ /dev/null @@ -1,955 +0,0 @@ -## API Report File for "@firebase/vertexai" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { AppCheckTokenResult } from '@firebase/app-check-interop-types'; -import { FirebaseApp } from '@firebase/app'; -import { FirebaseAuthTokenData } from '@firebase/auth-interop-types'; -import { FirebaseError } from '@firebase/util'; - -// @public -export interface AI { - app: FirebaseApp; - backend: Backend; - // @deprecated - location: string; -} - -// @public -export class AIError extends FirebaseError { - constructor(code: AIErrorCode, message: string, customErrorData?: CustomErrorData | undefined); - // (undocumented) - readonly code: AIErrorCode; - // (undocumented) - readonly customErrorData?: CustomErrorData | undefined; -} - -// @public -const enum AIErrorCode { - API_NOT_ENABLED = "api-not-enabled", - ERROR = "error", - FETCH_ERROR = "fetch-error", - INVALID_CONTENT = "invalid-content", - INVALID_SCHEMA = "invalid-schema", - NO_API_KEY = "no-api-key", - NO_APP_ID = "no-app-id", - NO_MODEL = "no-model", - NO_PROJECT_ID = "no-project-id", - PARSE_FAILED = "parse-failed", - REQUEST_ERROR = "request-error", - RESPONSE_ERROR = "response-error", - UNSUPPORTED = "unsupported" -} - -export { AIErrorCode } - -export { AIErrorCode as VertexAIErrorCode } - -// @public -export abstract class AIModel { - // @internal - protected constructor(ai: AI, modelName: string); - // Warning: (ae-forgotten-export) The symbol "ApiSettings" needs to be exported by the entry point index.d.ts - // - // @internal (undocumented) - protected _apiSettings: ApiSettings; - readonly model: string; - // @internal - static normalizeModelName(modelName: string, backendType: BackendType): string; - } - -// @public -export interface AIOptions { - backend: Backend; -} - -// @public -export class ArraySchema extends Schema { - constructor(schemaParams: SchemaParams, items: TypedSchema); - // (undocumented) - items: TypedSchema; - // @internal (undocumented) - toJSON(): SchemaRequest; -} - -// @public -export abstract class Backend { - protected constructor(type: BackendType); - readonly backendType: BackendType; -} - -// @public -export const BackendType: { - readonly VERTEX_AI: "VERTEX_AI"; - readonly GOOGLE_AI: "GOOGLE_AI"; -}; - -// @public -export type BackendType = (typeof BackendType)[keyof typeof BackendType]; - -// @public -export interface BaseParams { - // (undocumented) - generationConfig?: GenerationConfig; - // (undocumented) - safetySettings?: SafetySetting[]; -} - -// @public -export enum BlockReason { - BLOCKLIST = "BLOCKLIST", - OTHER = "OTHER", - PROHIBITED_CONTENT = "PROHIBITED_CONTENT", - SAFETY = "SAFETY" -} - -// @public -export class BooleanSchema extends Schema { - constructor(schemaParams?: SchemaParams); -} - -// @public -export class ChatSession { - constructor(apiSettings: ApiSettings, model: string, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined); - getHistory(): Promise; - // (undocumented) - model: string; - // (undocumented) - params?: StartChatParams | undefined; - // (undocumented) - requestOptions?: RequestOptions | undefined; - sendMessage(request: string | Array): Promise; - sendMessageStream(request: string | Array): Promise; - } - -// @public -export interface Citation { - // (undocumented) - endIndex?: number; - // (undocumented) - license?: string; - publicationDate?: Date_2; - // (undocumented) - startIndex?: number; - title?: string; - // (undocumented) - uri?: string; -} - -// @public -export interface CitationMetadata { - // (undocumented) - citations: Citation[]; -} - -// @public -export interface Content { - // (undocumented) - parts: Part[]; - // (undocumented) - role: Role; -} - -// @public -export interface CountTokensRequest { - // (undocumented) - contents: Content[]; - generationConfig?: GenerationConfig; - systemInstruction?: string | Part | Content; - tools?: Tool[]; -} - -// @public -export interface CountTokensResponse { - promptTokensDetails?: ModalityTokenCount[]; - totalBillableCharacters?: number; - totalTokens: number; -} - -// @public -export interface CustomErrorData { - errorDetails?: ErrorDetails[]; - response?: GenerateContentResponse; - status?: number; - statusText?: string; -} - -// @public -interface Date_2 { - // (undocumented) - day: number; - // (undocumented) - month: number; - // (undocumented) - year: number; -} - -export { Date_2 as Date } - -// @public -export interface EnhancedGenerateContentResponse extends GenerateContentResponse { - // (undocumented) - functionCalls: () => FunctionCall[] | undefined; - inlineDataParts: () => InlineDataPart[] | undefined; - text: () => string; -} - -// @public -export interface ErrorDetails { - // (undocumented) - '@type'?: string; - [key: string]: unknown; - domain?: string; - metadata?: Record; - reason?: string; -} - -// @public -export interface FileData { - // (undocumented) - fileUri: string; - // (undocumented) - mimeType: string; -} - -// @public -export interface FileDataPart { - // (undocumented) - fileData: FileData; - // (undocumented) - functionCall?: never; - // (undocumented) - functionResponse?: never; - // (undocumented) - inlineData?: never; - // (undocumented) - text?: never; -} - -// @public -export enum FinishReason { - BLOCKLIST = "BLOCKLIST", - MALFORMED_FUNCTION_CALL = "MALFORMED_FUNCTION_CALL", - MAX_TOKENS = "MAX_TOKENS", - OTHER = "OTHER", - PROHIBITED_CONTENT = "PROHIBITED_CONTENT", - RECITATION = "RECITATION", - SAFETY = "SAFETY", - SPII = "SPII", - STOP = "STOP" -} - -// @public -export interface FunctionCall { - // (undocumented) - args: object; - // (undocumented) - name: string; -} - -// @public (undocumented) -export interface FunctionCallingConfig { - // (undocumented) - allowedFunctionNames?: string[]; - // (undocumented) - mode?: FunctionCallingMode; -} - -// @public (undocumented) -export enum FunctionCallingMode { - ANY = "ANY", - AUTO = "AUTO", - NONE = "NONE" -} - -// @public -export interface FunctionCallPart { - // (undocumented) - functionCall: FunctionCall; - // (undocumented) - functionResponse?: never; - // (undocumented) - inlineData?: never; - // (undocumented) - text?: never; -} - -// @public -export interface FunctionDeclaration { - description: string; - name: string; - parameters?: ObjectSchemaInterface; -} - -// @public -export interface FunctionDeclarationsTool { - functionDeclarations?: FunctionDeclaration[]; -} - -// @public -export interface FunctionResponse { - // (undocumented) - name: string; - // (undocumented) - response: object; -} - -// @public -export interface FunctionResponsePart { - // (undocumented) - functionCall?: never; - // (undocumented) - functionResponse: FunctionResponse; - // (undocumented) - inlineData?: never; - // (undocumented) - text?: never; -} - -// @public -export interface GenerateContentCandidate { - // (undocumented) - citationMetadata?: CitationMetadata; - // (undocumented) - content: Content; - // (undocumented) - finishMessage?: string; - // (undocumented) - finishReason?: FinishReason; - // (undocumented) - groundingMetadata?: GroundingMetadata; - // (undocumented) - index: number; - // (undocumented) - safetyRatings?: SafetyRating[]; -} - -// @public -export interface GenerateContentRequest extends BaseParams { - // (undocumented) - contents: Content[]; - // (undocumented) - systemInstruction?: string | Part | Content; - // (undocumented) - toolConfig?: ToolConfig; - // (undocumented) - tools?: Tool[]; -} - -// @public -export interface GenerateContentResponse { - // (undocumented) - candidates?: GenerateContentCandidate[]; - // (undocumented) - promptFeedback?: PromptFeedback; - // (undocumented) - usageMetadata?: UsageMetadata; -} - -// @public -export interface GenerateContentResult { - // (undocumented) - response: EnhancedGenerateContentResponse; -} - -// @public -export interface GenerateContentStreamResult { - // (undocumented) - response: Promise; - // (undocumented) - stream: AsyncGenerator; -} - -// @public -export interface GenerationConfig { - // (undocumented) - candidateCount?: number; - // (undocumented) - frequencyPenalty?: number; - // (undocumented) - maxOutputTokens?: number; - // (undocumented) - presencePenalty?: number; - responseMimeType?: string; - // @beta - responseModalities?: ResponseModality[]; - responseSchema?: TypedSchema | SchemaRequest; - // (undocumented) - stopSequences?: string[]; - // (undocumented) - temperature?: number; - // (undocumented) - topK?: number; - // (undocumented) - topP?: number; -} - -// @public -export interface GenerativeContentBlob { - data: string; - // (undocumented) - mimeType: string; -} - -// @public -export class GenerativeModel extends AIModel { - constructor(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions); - countTokens(request: CountTokensRequest | string | Array): Promise; - generateContent(request: GenerateContentRequest | string | Array): Promise; - generateContentStream(request: GenerateContentRequest | string | Array): Promise; - // (undocumented) - generationConfig: GenerationConfig; - // (undocumented) - requestOptions?: RequestOptions; - // (undocumented) - safetySettings: SafetySetting[]; - startChat(startChatParams?: StartChatParams): ChatSession; - // (undocumented) - systemInstruction?: Content; - // (undocumented) - toolConfig?: ToolConfig; - // (undocumented) - tools?: Tool[]; -} - -// @public -export function getAI(app?: FirebaseApp, options?: AIOptions): AI; - -// @public -export function getGenerativeModel(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions): GenerativeModel; - -// @beta -export function getImagenModel(ai: AI, modelParams: ImagenModelParams, requestOptions?: RequestOptions): ImagenModel; - -// @public -export function getVertexAI(app?: FirebaseApp, options?: VertexAIOptions): VertexAI; - -// @public -export class GoogleAIBackend extends Backend { - constructor(); -} - -// Warning: (ae-internal-missing-underscore) The name "GoogleAICitationMetadata" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface GoogleAICitationMetadata { - // (undocumented) - citationSources: Citation[]; -} - -// Warning: (ae-internal-missing-underscore) The name "GoogleAICountTokensRequest" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface GoogleAICountTokensRequest { - // (undocumented) - generateContentRequest: { - model: string; - contents: Content[]; - systemInstruction?: string | Part | Content; - tools?: Tool[]; - generationConfig?: GenerationConfig; - }; -} - -// Warning: (ae-internal-missing-underscore) The name "GoogleAIGenerateContentCandidate" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface GoogleAIGenerateContentCandidate { - // (undocumented) - citationMetadata?: GoogleAICitationMetadata; - // (undocumented) - content: Content; - // (undocumented) - finishMessage?: string; - // (undocumented) - finishReason?: FinishReason; - // (undocumented) - groundingMetadata?: GroundingMetadata; - // (undocumented) - index: number; - // (undocumented) - safetyRatings?: SafetyRating[]; -} - -// Warning: (ae-internal-missing-underscore) The name "GoogleAIGenerateContentResponse" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface GoogleAIGenerateContentResponse { - // (undocumented) - candidates?: GoogleAIGenerateContentCandidate[]; - // (undocumented) - promptFeedback?: PromptFeedback; - // (undocumented) - usageMetadata?: UsageMetadata; -} - -// @public @deprecated (undocumented) -export interface GroundingAttribution { - // (undocumented) - confidenceScore?: number; - // (undocumented) - retrievedContext?: RetrievedContextAttribution; - // (undocumented) - segment: Segment; - // (undocumented) - web?: WebAttribution; -} - -// @public -export interface GroundingMetadata { - // @deprecated (undocumented) - groundingAttributions: GroundingAttribution[]; - // (undocumented) - retrievalQueries?: string[]; - // (undocumented) - webSearchQueries?: string[]; -} - -// @public -export enum HarmBlockMethod { - PROBABILITY = "PROBABILITY", - SEVERITY = "SEVERITY" -} - -// @public -export enum HarmBlockThreshold { - BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE", - BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE", - BLOCK_NONE = "BLOCK_NONE", - BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH" -} - -// @public -export enum HarmCategory { - // (undocumented) - HARM_CATEGORY_DANGEROUS_CONTENT = "HARM_CATEGORY_DANGEROUS_CONTENT", - // (undocumented) - HARM_CATEGORY_HARASSMENT = "HARM_CATEGORY_HARASSMENT", - // (undocumented) - HARM_CATEGORY_HATE_SPEECH = "HARM_CATEGORY_HATE_SPEECH", - // (undocumented) - HARM_CATEGORY_SEXUALLY_EXPLICIT = "HARM_CATEGORY_SEXUALLY_EXPLICIT" -} - -// @public -export enum HarmProbability { - HIGH = "HIGH", - LOW = "LOW", - MEDIUM = "MEDIUM", - NEGLIGIBLE = "NEGLIGIBLE" -} - -// @public -export enum HarmSeverity { - HARM_SEVERITY_HIGH = "HARM_SEVERITY_HIGH", - HARM_SEVERITY_LOW = "HARM_SEVERITY_LOW", - HARM_SEVERITY_MEDIUM = "HARM_SEVERITY_MEDIUM", - HARM_SEVERITY_NEGLIGIBLE = "HARM_SEVERITY_NEGLIGIBLE", - HARM_SEVERITY_UNSUPPORTED = "HARM_SEVERITY_UNSUPPORTED" -} - -// @beta -export enum ImagenAspectRatio { - LANDSCAPE_16x9 = "16:9", - LANDSCAPE_3x4 = "3:4", - PORTRAIT_4x3 = "4:3", - PORTRAIT_9x16 = "9:16", - SQUARE = "1:1" -} - -// @public -export interface ImagenGCSImage { - gcsURI: string; - mimeType: string; -} - -// @beta -export interface ImagenGenerationConfig { - addWatermark?: boolean; - aspectRatio?: ImagenAspectRatio; - imageFormat?: ImagenImageFormat; - negativePrompt?: string; - numberOfImages?: number; -} - -// @beta -export interface ImagenGenerationResponse { - filteredReason?: string; - images: T[]; -} - -// @beta -export class ImagenImageFormat { - compressionQuality?: number; - static jpeg(compressionQuality?: number): ImagenImageFormat; - mimeType: string; - static png(): ImagenImageFormat; -} - -// @beta -export interface ImagenInlineImage { - bytesBase64Encoded: string; - mimeType: string; -} - -// @beta -export class ImagenModel extends AIModel { - constructor(ai: AI, modelParams: ImagenModelParams, requestOptions?: RequestOptions | undefined); - generateImages(prompt: string): Promise>; - // @internal - generateImagesGCS(prompt: string, gcsURI: string): Promise>; - generationConfig?: ImagenGenerationConfig; - // (undocumented) - requestOptions?: RequestOptions | undefined; - safetySettings?: ImagenSafetySettings; -} - -// @beta -export interface ImagenModelParams { - generationConfig?: ImagenGenerationConfig; - model: string; - safetySettings?: ImagenSafetySettings; -} - -// @beta -export enum ImagenPersonFilterLevel { - ALLOW_ADULT = "allow_adult", - ALLOW_ALL = "allow_all", - BLOCK_ALL = "dont_allow" -} - -// @beta -export enum ImagenSafetyFilterLevel { - BLOCK_LOW_AND_ABOVE = "block_low_and_above", - BLOCK_MEDIUM_AND_ABOVE = "block_medium_and_above", - BLOCK_NONE = "block_none", - BLOCK_ONLY_HIGH = "block_only_high" -} - -// @beta -export interface ImagenSafetySettings { - personFilterLevel?: ImagenPersonFilterLevel; - safetyFilterLevel?: ImagenSafetyFilterLevel; -} - -// @public -export interface InlineDataPart { - // (undocumented) - functionCall?: never; - // (undocumented) - functionResponse?: never; - // (undocumented) - inlineData: GenerativeContentBlob; - // (undocumented) - text?: never; - videoMetadata?: VideoMetadata; -} - -// @public -export class IntegerSchema extends Schema { - constructor(schemaParams?: SchemaParams); -} - -// @public -export enum Modality { - AUDIO = "AUDIO", - DOCUMENT = "DOCUMENT", - IMAGE = "IMAGE", - MODALITY_UNSPECIFIED = "MODALITY_UNSPECIFIED", - TEXT = "TEXT", - VIDEO = "VIDEO" -} - -// @public -export interface ModalityTokenCount { - modality: Modality; - tokenCount: number; -} - -// @public -export interface ModelParams extends BaseParams { - // (undocumented) - model: string; - // (undocumented) - systemInstruction?: string | Part | Content; - // (undocumented) - toolConfig?: ToolConfig; - // (undocumented) - tools?: Tool[]; -} - -// @public -export class NumberSchema extends Schema { - constructor(schemaParams?: SchemaParams); -} - -// @public -export class ObjectSchema extends Schema { - constructor(schemaParams: SchemaParams, properties: { - [k: string]: TypedSchema; - }, optionalProperties?: string[]); - // (undocumented) - optionalProperties: string[]; - // (undocumented) - properties: { - [k: string]: TypedSchema; - }; - // @internal (undocumented) - toJSON(): SchemaRequest; -} - -// @public -export interface ObjectSchemaInterface extends SchemaInterface { - // (undocumented) - optionalProperties?: string[]; - // (undocumented) - type: SchemaType.OBJECT; -} - -// @public -export type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | FileDataPart; - -// @public -export const POSSIBLE_ROLES: readonly ["user", "model", "function", "system"]; - -// @public -export interface PromptFeedback { - // (undocumented) - blockReason?: BlockReason; - blockReasonMessage?: string; - // (undocumented) - safetyRatings: SafetyRating[]; -} - -// @public -export interface RequestOptions { - baseUrl?: string; - timeout?: number; -} - -// @beta -export const ResponseModality: { - readonly TEXT: "TEXT"; - readonly IMAGE: "IMAGE"; -}; - -// @beta -export type ResponseModality = (typeof ResponseModality)[keyof typeof ResponseModality]; - -// @public (undocumented) -export interface RetrievedContextAttribution { - // (undocumented) - title: string; - // (undocumented) - uri: string; -} - -// @public -export type Role = (typeof POSSIBLE_ROLES)[number]; - -// @public -export interface SafetyRating { - // (undocumented) - blocked: boolean; - // (undocumented) - category: HarmCategory; - // (undocumented) - probability: HarmProbability; - probabilityScore: number; - severity: HarmSeverity; - severityScore: number; -} - -// @public -export interface SafetySetting { - // (undocumented) - category: HarmCategory; - method?: HarmBlockMethod; - // (undocumented) - threshold: HarmBlockThreshold; -} - -// @public -export abstract class Schema implements SchemaInterface { - constructor(schemaParams: SchemaInterface); - [key: string]: unknown; - // (undocumented) - static array(arrayParams: SchemaParams & { - items: Schema; - }): ArraySchema; - // (undocumented) - static boolean(booleanParams?: SchemaParams): BooleanSchema; - description?: string; - // (undocumented) - static enumString(stringParams: SchemaParams & { - enum: string[]; - }): StringSchema; - example?: unknown; - format?: string; - // (undocumented) - static integer(integerParams?: SchemaParams): IntegerSchema; - nullable: boolean; - // (undocumented) - static number(numberParams?: SchemaParams): NumberSchema; - // (undocumented) - static object(objectParams: SchemaParams & { - properties: { - [k: string]: Schema; - }; - optionalProperties?: string[]; - }): ObjectSchema; - // (undocumented) - static string(stringParams?: SchemaParams): StringSchema; - // @internal - toJSON(): SchemaRequest; - type: SchemaType; -} - -// @public -export interface SchemaInterface extends SchemaShared { - type: SchemaType; -} - -// @public -export interface SchemaParams extends SchemaShared { -} - -// @public -export interface SchemaRequest extends SchemaShared { - required?: string[]; - type: SchemaType; -} - -// @public -export interface SchemaShared { - // (undocumented) - [key: string]: unknown; - description?: string; - enum?: string[]; - example?: unknown; - format?: string; - items?: T; - nullable?: boolean; - properties?: { - [k: string]: T; - }; -} - -// @public -export enum SchemaType { - ARRAY = "array", - BOOLEAN = "boolean", - INTEGER = "integer", - NUMBER = "number", - OBJECT = "object", - STRING = "string" -} - -// @public (undocumented) -export interface Segment { - // (undocumented) - endIndex: number; - // (undocumented) - partIndex: number; - // (undocumented) - startIndex: number; -} - -// @public -export interface StartChatParams extends BaseParams { - // (undocumented) - history?: Content[]; - // (undocumented) - systemInstruction?: string | Part | Content; - // (undocumented) - toolConfig?: ToolConfig; - // (undocumented) - tools?: Tool[]; -} - -// @public -export class StringSchema extends Schema { - constructor(schemaParams?: SchemaParams, enumValues?: string[]); - // (undocumented) - enum?: string[]; - // @internal (undocumented) - toJSON(): SchemaRequest; -} - -// @public -export interface TextPart { - // (undocumented) - functionCall?: never; - // (undocumented) - functionResponse?: never; - // (undocumented) - inlineData?: never; - // (undocumented) - text: string; -} - -// @public -export type Tool = FunctionDeclarationsTool; - -// @public -export interface ToolConfig { - // (undocumented) - functionCallingConfig?: FunctionCallingConfig; -} - -// @public -export type TypedSchema = IntegerSchema | NumberSchema | StringSchema | BooleanSchema | ObjectSchema | ArraySchema; - -// @public -export interface UsageMetadata { - // (undocumented) - candidatesTokenCount: number; - // (undocumented) - candidatesTokensDetails?: ModalityTokenCount[]; - // (undocumented) - promptTokenCount: number; - // (undocumented) - promptTokensDetails?: ModalityTokenCount[]; - // (undocumented) - totalTokenCount: number; -} - -// @public -export type VertexAI = AI; - -// @public -export class VertexAIBackend extends Backend { - constructor(location?: string); - readonly location: string; -} - -// @public -export const VertexAIError: typeof AIError; - -// @public -export const VertexAIModel: typeof AIModel; - -// @public -export interface VertexAIOptions { - // (undocumented) - location?: string; -} - -// @public -export interface VideoMetadata { - endOffset: string; - startOffset: string; -} - -// @public (undocumented) -export interface WebAttribution { - // (undocumented) - title: string; - // (undocumented) - uri: string; -} - - -``` diff --git a/config/.eslintrc.js b/config/.eslintrc.js index 57243a3e2a4..8212432f841 100644 --- a/config/.eslintrc.js +++ b/config/.eslintrc.js @@ -31,7 +31,7 @@ module.exports = { 'unused-imports' ], 'parserOptions': { - 'ecmaVersion': 2017, + 'ecmaVersion': 2020, 'sourceType': 'module' }, 'overrides': [ diff --git a/config/functions/package.json b/config/functions/package.json index 9a032e8bb95..4b823c3de04 100644 --- a/config/functions/package.json +++ b/config/functions/package.json @@ -8,6 +8,6 @@ }, "private": true, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/config/tsconfig.base.json b/config/tsconfig.base.json index adbd2e44072..ce58a6d700b 100644 --- a/config/tsconfig.base.json +++ b/config/tsconfig.base.json @@ -6,7 +6,7 @@ "strict": true, "lib": [ "dom", - "es2017", + "es2020", "esnext.WeakRef", ], "module": "ES2015", @@ -14,7 +14,7 @@ "resolveJsonModule": true, "esModuleInterop": true, "sourceMap": true, - "target": "es2017", + "target": "es2020", "typeRoots": [ "../node_modules/@types" ], diff --git a/config/webpack.test.js b/config/webpack.test.js index d84ed2d209c..cc739313779 100644 --- a/config/webpack.test.js +++ b/config/webpack.test.js @@ -28,7 +28,7 @@ const PLATFORM_RE = /^(.*)\/platform\/([^.\/]*)(\.ts)?$/; module.exports = { mode: 'development', - devtool: 'source-map', + devtool: 'inline-source-map', optimization: { runtimeChunk: false, splitChunks: false, @@ -44,7 +44,7 @@ module.exports = { options: { compilerOptions: { module: 'commonjs', - target: 'es2017', + target: 'es2020', downlevelIteration: true, resolveJsonModule: true } @@ -56,15 +56,6 @@ module.exports = { use: 'source-map-loader', enforce: 'pre' }, - { - test: /\.tsx?$/, - use: { - loader: 'istanbul-instrumenter-loader', - options: { esModules: true } - }, - enforce: 'post', - exclude: [/\.test\.ts$/, /\btest(ing)?\//] - }, { test: /\.js$/, include: [/node_modules\/chai-as-promised/], diff --git a/docs-devsite/_toc.yaml b/docs-devsite/_toc.yaml index b77a6b5910e..04d65f6c333 100644 --- a/docs-devsite/_toc.yaml +++ b/docs-devsite/_toc.yaml @@ -12,8 +12,12 @@ toc: path: /docs/reference/js/ai.aimodel.md - title: AIOptions path: /docs/reference/js/ai.aioptions.md + - title: AnyOfSchema + path: /docs/reference/js/ai.anyofschema.md - title: ArraySchema path: /docs/reference/js/ai.arrayschema.md + - title: AudioConversationController + path: /docs/reference/js/ai.audioconversationcontroller.md - title: Backend path: /docs/reference/js/ai.backend.md - title: BaseParams @@ -22,10 +26,18 @@ toc: path: /docs/reference/js/ai.booleanschema.md - title: ChatSession path: /docs/reference/js/ai.chatsession.md + - title: ChromeAdapter + path: /docs/reference/js/ai.chromeadapter.md - title: Citation path: /docs/reference/js/ai.citation.md - title: CitationMetadata path: /docs/reference/js/ai.citationmetadata.md + - title: CodeExecutionResult + path: /docs/reference/js/ai.codeexecutionresult.md + - title: CodeExecutionResultPart + path: /docs/reference/js/ai.codeexecutionresultpart.md + - title: CodeExecutionTool + path: /docs/reference/js/ai.codeexecutiontool.md - title: Content path: /docs/reference/js/ai.content.md - title: CountTokensRequest @@ -40,6 +52,10 @@ toc: path: /docs/reference/js/ai.enhancedgeneratecontentresponse.md - title: ErrorDetails path: /docs/reference/js/ai.errordetails.md + - title: ExecutableCode + path: /docs/reference/js/ai.executablecode.md + - title: ExecutableCodePart + path: /docs/reference/js/ai.executablecodepart.md - title: FileData path: /docs/reference/js/ai.filedata.md - title: FileDataPart @@ -76,10 +92,18 @@ toc: path: /docs/reference/js/ai.generativemodel.md - title: GoogleAIBackend path: /docs/reference/js/ai.googleaibackend.md - - title: GroundingAttribution - path: /docs/reference/js/ai.groundingattribution.md + - title: GoogleSearch + path: /docs/reference/js/ai.googlesearch.md + - title: GoogleSearchTool + path: /docs/reference/js/ai.googlesearchtool.md + - title: GroundingChunk + path: /docs/reference/js/ai.groundingchunk.md - title: GroundingMetadata path: /docs/reference/js/ai.groundingmetadata.md + - title: GroundingSupport + path: /docs/reference/js/ai.groundingsupport.md + - title: HybridParams + path: /docs/reference/js/ai.hybridparams.md - title: ImagenGCSImage path: /docs/reference/js/ai.imagengcsimage.md - title: ImagenGenerationConfig @@ -100,6 +124,32 @@ toc: path: /docs/reference/js/ai.inlinedatapart.md - title: IntegerSchema path: /docs/reference/js/ai.integerschema.md + - title: LanguageModelCreateCoreOptions + path: /docs/reference/js/ai.languagemodelcreatecoreoptions.md + - title: LanguageModelCreateOptions + path: /docs/reference/js/ai.languagemodelcreateoptions.md + - title: LanguageModelExpected + path: /docs/reference/js/ai.languagemodelexpected.md + - title: LanguageModelMessage + path: /docs/reference/js/ai.languagemodelmessage.md + - title: LanguageModelMessageContent + path: /docs/reference/js/ai.languagemodelmessagecontent.md + - title: LanguageModelPromptOptions + path: /docs/reference/js/ai.languagemodelpromptoptions.md + - title: LiveGenerationConfig + path: /docs/reference/js/ai.livegenerationconfig.md + - title: LiveGenerativeModel + path: /docs/reference/js/ai.livegenerativemodel.md + - title: LiveModelParams + path: /docs/reference/js/ai.livemodelparams.md + - title: LiveServerContent + path: /docs/reference/js/ai.liveservercontent.md + - title: LiveServerToolCall + path: /docs/reference/js/ai.liveservertoolcall.md + - title: LiveServerToolCallCancellation + path: /docs/reference/js/ai.liveservertoolcallcancellation.md + - title: LiveSession + path: /docs/reference/js/ai.livesession.md - title: ModalityTokenCount path: /docs/reference/js/ai.modalitytokencount.md - title: ModelParams @@ -108,8 +158,12 @@ toc: path: /docs/reference/js/ai.numberschema.md - title: ObjectSchema path: /docs/reference/js/ai.objectschema.md - - title: ObjectSchemaInterface - path: /docs/reference/js/ai.objectschemainterface.md + - title: ObjectSchemaRequest + path: /docs/reference/js/ai.objectschemarequest.md + - title: OnDeviceParams + path: /docs/reference/js/ai.ondeviceparams.md + - title: PrebuiltVoiceConfig + path: /docs/reference/js/ai.prebuiltvoiceconfig.md - title: PromptFeedback path: /docs/reference/js/ai.promptfeedback.md - title: RequestOptions @@ -130,26 +184,44 @@ toc: path: /docs/reference/js/ai.schemarequest.md - title: SchemaShared path: /docs/reference/js/ai.schemashared.md + - title: SearchEntrypoint + path: /docs/reference/js/ai.searchentrypoint.md - title: Segment path: /docs/reference/js/ai.segment.md + - title: SpeechConfig + path: /docs/reference/js/ai.speechconfig.md + - title: StartAudioConversationOptions + path: /docs/reference/js/ai.startaudioconversationoptions.md - title: StartChatParams path: /docs/reference/js/ai.startchatparams.md - title: StringSchema path: /docs/reference/js/ai.stringschema.md - title: TextPart path: /docs/reference/js/ai.textpart.md + - title: ThinkingConfig + path: /docs/reference/js/ai.thinkingconfig.md - title: ToolConfig path: /docs/reference/js/ai.toolconfig.md + - title: URLContext + path: /docs/reference/js/ai.urlcontext.md + - title: URLContextMetadata + path: /docs/reference/js/ai.urlcontextmetadata.md + - title: URLContextTool + path: /docs/reference/js/ai.urlcontexttool.md + - title: URLMetadata + path: /docs/reference/js/ai.urlmetadata.md - title: UsageMetadata path: /docs/reference/js/ai.usagemetadata.md - title: VertexAIBackend path: /docs/reference/js/ai.vertexaibackend.md - - title: VertexAIOptions - path: /docs/reference/js/ai.vertexaioptions.md - title: VideoMetadata path: /docs/reference/js/ai.videometadata.md + - title: VoiceConfig + path: /docs/reference/js/ai.voiceconfig.md - title: WebAttribution path: /docs/reference/js/ai.webattribution.md + - title: WebGroundingChunk + path: /docs/reference/js/ai.webgroundingchunk.md - title: analytics path: /docs/reference/js/analytics.md section: @@ -577,6 +649,10 @@ toc: - title: remote-config path: /docs/reference/js/remote-config.md section: + - title: ConfigUpdate + path: /docs/reference/js/remote-config.configupdate.md + - title: ConfigUpdateObserver + path: /docs/reference/js/remote-config.configupdateobserver.md - title: CustomSignals path: /docs/reference/js/remote-config.customsignals.md - title: FetchResponse diff --git a/docs-devsite/ai.ai.md b/docs-devsite/ai.ai.md index d4127ffb7e8..d7aafb6a5b5 100644 --- a/docs-devsite/ai.ai.md +++ b/docs-devsite/ai.ai.md @@ -27,6 +27,7 @@ export interface AI | [app](./ai.ai.md#aiapp) | [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) | The [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) this [AI](./ai.ai.md#ai_interface) instance is associated with. | | [backend](./ai.ai.md#aibackend) | [Backend](./ai.backend.md#backend_class) | A [Backend](./ai.backend.md#backend_class) instance that specifies the configuration for the target backend, either the Gemini Developer API (using [GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)) or the Vertex AI Gemini API (using [VertexAIBackend](./ai.vertexaibackend.md#vertexaibackend_class)). | | [location](./ai.ai.md#ailocation) | string | | +| [options](./ai.ai.md#aioptions) | [AIOptions](./ai.aioptions.md#aioptions_interface) | Options applied to this [AI](./ai.ai.md#ai_interface) instance. | ## AI.app @@ -62,3 +63,13 @@ backend: Backend; ```typescript location: string; ``` + +## AI.options + +Options applied to this [AI](./ai.ai.md#ai_interface) instance. + +Signature: + +```typescript +options?: AIOptions; +``` diff --git a/docs-devsite/ai.aioptions.md b/docs-devsite/ai.aioptions.md index a092046900b..a5b326ef004 100644 --- a/docs-devsite/ai.aioptions.md +++ b/docs-devsite/ai.aioptions.md @@ -22,14 +22,25 @@ export interface AIOptions | Property | Type | Description | | --- | --- | --- | -| [backend](./ai.aioptions.md#aioptionsbackend) | [Backend](./ai.backend.md#backend_class) | The backend configuration to use for the AI service instance. | +| [backend](./ai.aioptions.md#aioptionsbackend) | [Backend](./ai.backend.md#backend_class) | The backend configuration to use for the AI service instance. Defaults to the Gemini Developer API backend ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). | +| [useLimitedUseAppCheckTokens](./ai.aioptions.md#aioptionsuselimiteduseappchecktokens) | boolean | Whether to use App Check limited use tokens. Defaults to false. | ## AIOptions.backend -The backend configuration to use for the AI service instance. +The backend configuration to use for the AI service instance. Defaults to the Gemini Developer API backend ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). Signature: ```typescript -backend: Backend; +backend?: Backend; +``` + +## AIOptions.useLimitedUseAppCheckTokens + +Whether to use App Check limited use tokens. Defaults to false. + +Signature: + +```typescript +useLimitedUseAppCheckTokens?: boolean; ``` diff --git a/docs-devsite/ai.anyofschema.md b/docs-devsite/ai.anyofschema.md new file mode 100644 index 00000000000..6fc0fbc60a1 --- /dev/null +++ b/docs-devsite/ai.anyofschema.md @@ -0,0 +1,58 @@ +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 %} + +# AnyOfSchema class +Schema class representing a value that can conform to any of the provided sub-schemas. This is useful when a field can accept multiple distinct types or structures. + +Signature: + +```typescript +export declare class AnyOfSchema extends Schema +``` +Extends: [Schema](./ai.schema.md#schema_class) + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(schemaParams)](./ai.anyofschema.md#anyofschemaconstructor) | | Constructs a new instance of the AnyOfSchema class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [anyOf](./ai.anyofschema.md#anyofschemaanyof) | | [TypedSchema](./ai.md#typedschema)\[\] | | + +## AnyOfSchema.(constructor) + +Constructs a new instance of the `AnyOfSchema` class + +Signature: + +```typescript +constructor(schemaParams: SchemaParams & { + anyOf: TypedSchema[]; + }); +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| schemaParams | [SchemaParams](./ai.schemaparams.md#schemaparams_interface) & { anyOf: [TypedSchema](./ai.md#typedschema)\[\]; } | | + +## AnyOfSchema.anyOf + +Signature: + +```typescript +anyOf: TypedSchema[]; +``` diff --git a/docs-devsite/ai.audioconversationcontroller.md b/docs-devsite/ai.audioconversationcontroller.md new file mode 100644 index 00000000000..18820a2fe55 --- /dev/null +++ b/docs-devsite/ai.audioconversationcontroller.md @@ -0,0 +1,41 @@ +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 %} + +# AudioConversationController interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +A controller for managing an active audio conversation. + +Signature: + +```typescript +export interface AudioConversationController +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [stop](./ai.audioconversationcontroller.md#audioconversationcontrollerstop) | () => Promise<void> | (Public Preview) Stops the audio conversation, closes the microphone connection, and cleans up resources. Returns a promise that resolves when cleanup is complete. | + +## AudioConversationController.stop + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Stops the audio conversation, closes the microphone connection, and cleans up resources. Returns a promise that resolves when cleanup is complete. + +Signature: + +```typescript +stop: () => Promise; +``` diff --git a/docs-devsite/ai.chatsession.md b/docs-devsite/ai.chatsession.md index 1d6e403b6a8..4e4358898a5 100644 --- a/docs-devsite/ai.chatsession.md +++ b/docs-devsite/ai.chatsession.md @@ -22,7 +22,7 @@ export declare class ChatSession | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(apiSettings, model, params, requestOptions)](./ai.chatsession.md#chatsessionconstructor) | | Constructs a new instance of the ChatSession class | +| [(constructor)(apiSettings, model, chromeAdapter, params, requestOptions)](./ai.chatsession.md#chatsessionconstructor) | | Constructs a new instance of the ChatSession class | ## Properties @@ -47,7 +47,7 @@ Constructs a new instance of the `ChatSession` class Signature: ```typescript -constructor(apiSettings: ApiSettings, model: string, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined); +constructor(apiSettings: ApiSettings, model: string, chromeAdapter?: ChromeAdapter | undefined, params?: StartChatParams | undefined, requestOptions?: RequestOptions | undefined); ``` #### Parameters @@ -56,6 +56,7 @@ constructor(apiSettings: ApiSettings, model: string, params?: StartChatParams | | --- | --- | --- | | apiSettings | ApiSettings | | | model | string | | +| chromeAdapter | [ChromeAdapter](./ai.chromeadapter.md#chromeadapter_interface) \| undefined | | | params | [StartChatParams](./ai.startchatparams.md#startchatparams_interface) \| undefined | | | requestOptions | [RequestOptions](./ai.requestoptions.md#requestoptions_interface) \| undefined | | diff --git a/docs-devsite/ai.chromeadapter.md b/docs-devsite/ai.chromeadapter.md new file mode 100644 index 00000000000..e9a7a512503 --- /dev/null +++ b/docs-devsite/ai.chromeadapter.md @@ -0,0 +1,106 @@ +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 %} + +# ChromeAdapter interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Defines an inference "backend" that uses Chrome's on-device model, and encapsulates logic for detecting when on-device inference is possible. + +These methods should not be called directly by the user. + +Signature: + +```typescript +export interface ChromeAdapter +``` + +## Methods + +| Method | Description | +| --- | --- | +| [generateContent(request)](./ai.chromeadapter.md#chromeadaptergeneratecontent) | (Public Preview) Generates content using on-device inference. | +| [generateContentStream(request)](./ai.chromeadapter.md#chromeadaptergeneratecontentstream) | (Public Preview) Generates a content stream using on-device inference. | +| [isAvailable(request)](./ai.chromeadapter.md#chromeadapterisavailable) | (Public Preview) Checks if the on-device model is capable of handling a given request. | + +## ChromeAdapter.generateContent() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Generates content using on-device inference. + +This is comparable to [GenerativeModel.generateContent()](./ai.generativemodel.md#generativemodelgeneratecontent) for generating content using in-cloud inference. + +Signature: + +```typescript +generateContent(request: GenerateContentRequest): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| request | [GenerateContentRequest](./ai.generatecontentrequest.md#generatecontentrequest_interface) | a standard Firebase AI [GenerateContentRequest](./ai.generatecontentrequest.md#generatecontentrequest_interface) | + +Returns: + +Promise<Response> + +## ChromeAdapter.generateContentStream() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Generates a content stream using on-device inference. + +This is comparable to [GenerativeModel.generateContentStream()](./ai.generativemodel.md#generativemodelgeneratecontentstream) for generating a content stream using in-cloud inference. + +Signature: + +```typescript +generateContentStream(request: GenerateContentRequest): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| request | [GenerateContentRequest](./ai.generatecontentrequest.md#generatecontentrequest_interface) | a standard Firebase AI [GenerateContentRequest](./ai.generatecontentrequest.md#generatecontentrequest_interface) | + +Returns: + +Promise<Response> + +## ChromeAdapter.isAvailable() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Checks if the on-device model is capable of handling a given request. + +Signature: + +```typescript +isAvailable(request: GenerateContentRequest): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| request | [GenerateContentRequest](./ai.generatecontentrequest.md#generatecontentrequest_interface) | A potential request to be passed to the model. | + +Returns: + +Promise<boolean> + diff --git a/docs-devsite/ai.codeexecutionresult.md b/docs-devsite/ai.codeexecutionresult.md new file mode 100644 index 00000000000..d9d937ecad6 --- /dev/null +++ b/docs-devsite/ai.codeexecutionresult.md @@ -0,0 +1,55 @@ +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 %} + +# CodeExecutionResult interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The results of code execution run by the model. + +Signature: + +```typescript +export interface CodeExecutionResult +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [outcome](./ai.codeexecutionresult.md#codeexecutionresultoutcome) | [Outcome](./ai.md#outcome) | (Public Preview) The result of the code execution. | +| [output](./ai.codeexecutionresult.md#codeexecutionresultoutput) | string | (Public Preview) The output from the code execution, or an error message if it failed. | + +## CodeExecutionResult.outcome + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The result of the code execution. + +Signature: + +```typescript +outcome?: Outcome; +``` + +## CodeExecutionResult.output + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The output from the code execution, or an error message if it failed. + +Signature: + +```typescript +output?: string; +``` diff --git a/docs-devsite/ai.codeexecutionresultpart.md b/docs-devsite/ai.codeexecutionresultpart.md new file mode 100644 index 00000000000..19364c5a7b7 --- /dev/null +++ b/docs-devsite/ai.codeexecutionresultpart.md @@ -0,0 +1,123 @@ +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 %} + +# CodeExecutionResultPart interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Represents the code execution result from the model. + +Signature: + +```typescript +export interface CodeExecutionResultPart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [codeExecutionResult](./ai.codeexecutionresultpart.md#codeexecutionresultpartcodeexecutionresult) | [CodeExecutionResult](./ai.codeexecutionresult.md#codeexecutionresult_interface) | (Public Preview) | +| [executableCode](./ai.codeexecutionresultpart.md#codeexecutionresultpartexecutablecode) | never | (Public Preview) | +| [fileData](./ai.codeexecutionresultpart.md#codeexecutionresultpartfiledata) | never | (Public Preview) | +| [functionCall](./ai.codeexecutionresultpart.md#codeexecutionresultpartfunctioncall) | never | (Public Preview) | +| [functionResponse](./ai.codeexecutionresultpart.md#codeexecutionresultpartfunctionresponse) | never | (Public Preview) | +| [inlineData](./ai.codeexecutionresultpart.md#codeexecutionresultpartinlinedata) | never | (Public Preview) | +| [text](./ai.codeexecutionresultpart.md#codeexecutionresultparttext) | never | (Public Preview) | +| [thought](./ai.codeexecutionresultpart.md#codeexecutionresultpartthought) | never | (Public Preview) | + +## CodeExecutionResultPart.codeExecutionResult + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +codeExecutionResult?: CodeExecutionResult; +``` + +## CodeExecutionResultPart.executableCode + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +executableCode?: never; +``` + +## CodeExecutionResultPart.fileData + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +fileData: never; +``` + +## CodeExecutionResultPart.functionCall + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +functionCall?: never; +``` + +## CodeExecutionResultPart.functionResponse + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +functionResponse?: never; +``` + +## CodeExecutionResultPart.inlineData + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +inlineData?: never; +``` + +## CodeExecutionResultPart.text + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +text?: never; +``` + +## CodeExecutionResultPart.thought + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +thought?: never; +``` diff --git a/docs-devsite/ai.codeexecutiontool.md b/docs-devsite/ai.codeexecutiontool.md new file mode 100644 index 00000000000..68a1e133d7b --- /dev/null +++ b/docs-devsite/ai.codeexecutiontool.md @@ -0,0 +1,41 @@ +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 %} + +# CodeExecutionTool interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +A tool that enables the model to use code execution. + +Signature: + +```typescript +export interface CodeExecutionTool +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [codeExecution](./ai.codeexecutiontool.md#codeexecutiontoolcodeexecution) | {} | (Public Preview) Specifies the Google Search configuration. Currently, this is an empty object, but it's reserved for future configuration options. | + +## CodeExecutionTool.codeExecution + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Specifies the Google Search configuration. Currently, this is an empty object, but it's reserved for future configuration options. + +Signature: + +```typescript +codeExecution: {}; +``` diff --git a/docs-devsite/ai.enhancedgeneratecontentresponse.md b/docs-devsite/ai.enhancedgeneratecontentresponse.md index 330dc10f322..9e947add0cb 100644 --- a/docs-devsite/ai.enhancedgeneratecontentresponse.md +++ b/docs-devsite/ai.enhancedgeneratecontentresponse.md @@ -23,12 +23,15 @@ export interface EnhancedGenerateContentResponse extends GenerateContentResponse | Property | Type | Description | | --- | --- | --- | -| [functionCalls](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponsefunctioncalls) | () => [FunctionCall](./ai.functioncall.md#functioncall_interface)\[\] \| undefined | | -| [inlineDataParts](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponseinlinedataparts) | () => [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface)\[\] \| undefined | Aggregates and returns all [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface)s from the [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface)'s first candidate. | +| [functionCalls](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponsefunctioncalls) | () => [FunctionCall](./ai.functioncall.md#functioncall_interface)\[\] \| undefined | Aggregates and returns every [FunctionCall](./ai.functioncall.md#functioncall_interface) from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | +| [inlineDataParts](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponseinlinedataparts) | () => [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface)\[\] \| undefined | Aggregates and returns every [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface) from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | | [text](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponsetext) | () => string | Returns the text string from the response, if available. Throws if the prompt or candidate was blocked. | +| [thoughtSummary](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponsethoughtsummary) | () => string \| undefined | Aggregates and returns every [TextPart](./ai.textpart.md#textpart_interface) with their thought property set to true from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | ## EnhancedGenerateContentResponse.functionCalls +Aggregates and returns every [FunctionCall](./ai.functioncall.md#functioncall_interface) from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). + Signature: ```typescript @@ -37,7 +40,7 @@ functionCalls: () => FunctionCall[] | undefined; ## EnhancedGenerateContentResponse.inlineDataParts -Aggregates and returns all [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface)s from the [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface)'s first candidate. +Aggregates and returns every [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface) from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). Signature: @@ -54,3 +57,17 @@ Returns the text string from the response, if available. Throws if the prompt or ```typescript text: () => string; ``` + +## EnhancedGenerateContentResponse.thoughtSummary + +Aggregates and returns every [TextPart](./ai.textpart.md#textpart_interface) with their `thought` property set to `true` from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). + +Thought summaries provide a brief overview of the model's internal thinking process, offering insight into how it arrived at the final answer. This can be useful for debugging, understanding the model's reasoning, and verifying its accuracy. + +Thoughts will only be included if [ThinkingConfig.includeThoughts](./ai.thinkingconfig.md#thinkingconfigincludethoughts) is set to `true`. + +Signature: + +```typescript +thoughtSummary: () => string | undefined; +``` diff --git a/docs-devsite/ai.executablecode.md b/docs-devsite/ai.executablecode.md new file mode 100644 index 00000000000..c2dfa09cd77 --- /dev/null +++ b/docs-devsite/ai.executablecode.md @@ -0,0 +1,55 @@ +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 %} + +# ExecutableCode interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +An interface for executable code returned by the model. + +Signature: + +```typescript +export interface ExecutableCode +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [code](./ai.executablecode.md#executablecodecode) | string | (Public Preview) The source code to be executed. | +| [language](./ai.executablecode.md#executablecodelanguage) | [Language](./ai.md#language) | (Public Preview) The programming language of the code. | + +## ExecutableCode.code + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The source code to be executed. + +Signature: + +```typescript +code?: string; +``` + +## ExecutableCode.language + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The programming language of the code. + +Signature: + +```typescript +language?: Language; +``` diff --git a/docs-devsite/ai.executablecodepart.md b/docs-devsite/ai.executablecodepart.md new file mode 100644 index 00000000000..cac32c132ed --- /dev/null +++ b/docs-devsite/ai.executablecodepart.md @@ -0,0 +1,123 @@ +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 %} + +# ExecutableCodePart interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Represents the code that is executed by the model. + +Signature: + +```typescript +export interface ExecutableCodePart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [codeExecutionResult](./ai.executablecodepart.md#executablecodepartcodeexecutionresult) | never | (Public Preview) | +| [executableCode](./ai.executablecodepart.md#executablecodepartexecutablecode) | [ExecutableCode](./ai.executablecode.md#executablecode_interface) | (Public Preview) | +| [fileData](./ai.executablecodepart.md#executablecodepartfiledata) | never | (Public Preview) | +| [functionCall](./ai.executablecodepart.md#executablecodepartfunctioncall) | never | (Public Preview) | +| [functionResponse](./ai.executablecodepart.md#executablecodepartfunctionresponse) | never | (Public Preview) | +| [inlineData](./ai.executablecodepart.md#executablecodepartinlinedata) | never | (Public Preview) | +| [text](./ai.executablecodepart.md#executablecodeparttext) | never | (Public Preview) | +| [thought](./ai.executablecodepart.md#executablecodepartthought) | never | (Public Preview) | + +## ExecutableCodePart.codeExecutionResult + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +codeExecutionResult?: never; +``` + +## ExecutableCodePart.executableCode + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +executableCode?: ExecutableCode; +``` + +## ExecutableCodePart.fileData + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +fileData: never; +``` + +## ExecutableCodePart.functionCall + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +functionCall?: never; +``` + +## ExecutableCodePart.functionResponse + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +functionResponse?: never; +``` + +## ExecutableCodePart.inlineData + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +inlineData?: never; +``` + +## ExecutableCodePart.text + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +text?: never; +``` + +## ExecutableCodePart.thought + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +thought?: never; +``` diff --git a/docs-devsite/ai.filedatapart.md b/docs-devsite/ai.filedatapart.md index 65cb9dc00ef..f031988a993 100644 --- a/docs-devsite/ai.filedatapart.md +++ b/docs-devsite/ai.filedatapart.md @@ -22,11 +22,30 @@ export interface FileDataPart | Property | Type | Description | | --- | --- | --- | +| [codeExecutionResult](./ai.filedatapart.md#filedatapartcodeexecutionresult) | never | | +| [executableCode](./ai.filedatapart.md#filedatapartexecutablecode) | never | | | [fileData](./ai.filedatapart.md#filedatapartfiledata) | [FileData](./ai.filedata.md#filedata_interface) | | | [functionCall](./ai.filedatapart.md#filedatapartfunctioncall) | never | | | [functionResponse](./ai.filedatapart.md#filedatapartfunctionresponse) | never | | | [inlineData](./ai.filedatapart.md#filedatapartinlinedata) | never | | | [text](./ai.filedatapart.md#filedataparttext) | never | | +| [thought](./ai.filedatapart.md#filedatapartthought) | boolean | | + +## FileDataPart.codeExecutionResult + +Signature: + +```typescript +codeExecutionResult?: never; +``` + +## FileDataPart.executableCode + +Signature: + +```typescript +executableCode?: never; +``` ## FileDataPart.fileData @@ -67,3 +86,11 @@ inlineData?: never; ```typescript text?: never; ``` + +## FileDataPart.thought + +Signature: + +```typescript +thought?: boolean; +``` diff --git a/docs-devsite/ai.functioncall.md b/docs-devsite/ai.functioncall.md index 1c789784fe1..b0bb2424a10 100644 --- a/docs-devsite/ai.functioncall.md +++ b/docs-devsite/ai.functioncall.md @@ -23,6 +23,7 @@ export interface FunctionCall | Property | Type | Description | | --- | --- | --- | | [args](./ai.functioncall.md#functioncallargs) | object | | +| [id](./ai.functioncall.md#functioncallid) | string | The id of the function call. This must be sent back in the associated [FunctionResponse](./ai.functionresponse.md#functionresponse_interface). | | [name](./ai.functioncall.md#functioncallname) | string | | ## FunctionCall.args @@ -33,6 +34,18 @@ export interface FunctionCall args: object; ``` +## FunctionCall.id + +The id of the function call. This must be sent back in the associated [FunctionResponse](./ai.functionresponse.md#functionresponse_interface). + +This property is only supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). When using the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)), this property will be `undefined`. + +Signature: + +```typescript +id?: string; +``` + ## FunctionCall.name Signature: diff --git a/docs-devsite/ai.functioncallpart.md b/docs-devsite/ai.functioncallpart.md index b16e58f80a6..cb9d8b89cf2 100644 --- a/docs-devsite/ai.functioncallpart.md +++ b/docs-devsite/ai.functioncallpart.md @@ -22,10 +22,29 @@ export interface FunctionCallPart | Property | Type | Description | | --- | --- | --- | +| [codeExecutionResult](./ai.functioncallpart.md#functioncallpartcodeexecutionresult) | never | | +| [executableCode](./ai.functioncallpart.md#functioncallpartexecutablecode) | never | | | [functionCall](./ai.functioncallpart.md#functioncallpartfunctioncall) | [FunctionCall](./ai.functioncall.md#functioncall_interface) | | | [functionResponse](./ai.functioncallpart.md#functioncallpartfunctionresponse) | never | | | [inlineData](./ai.functioncallpart.md#functioncallpartinlinedata) | never | | | [text](./ai.functioncallpart.md#functioncallparttext) | never | | +| [thought](./ai.functioncallpart.md#functioncallpartthought) | boolean | | + +## FunctionCallPart.codeExecutionResult + +Signature: + +```typescript +codeExecutionResult?: never; +``` + +## FunctionCallPart.executableCode + +Signature: + +```typescript +executableCode?: never; +``` ## FunctionCallPart.functionCall @@ -58,3 +77,11 @@ inlineData?: never; ```typescript text?: never; ``` + +## FunctionCallPart.thought + +Signature: + +```typescript +thought?: boolean; +``` diff --git a/docs-devsite/ai.functiondeclaration.md b/docs-devsite/ai.functiondeclaration.md index 2a87d67ed47..460c9792655 100644 --- a/docs-devsite/ai.functiondeclaration.md +++ b/docs-devsite/ai.functiondeclaration.md @@ -15,7 +15,7 @@ Structured representation of a function declaration as defined by the [OpenAPI 3 Signature: ```typescript -export declare interface FunctionDeclaration +export interface FunctionDeclaration ``` ## Properties @@ -24,7 +24,7 @@ export declare interface FunctionDeclaration | --- | --- | --- | | [description](./ai.functiondeclaration.md#functiondeclarationdescription) | string | Description and purpose of the function. Model uses it to decide how and whether to call the function. | | [name](./ai.functiondeclaration.md#functiondeclarationname) | string | The name of the function to call. Must start with a letter or an underscore. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a max length of 64. | -| [parameters](./ai.functiondeclaration.md#functiondeclarationparameters) | [ObjectSchemaInterface](./ai.objectschemainterface.md#objectschemainterface_interface) | Optional. Describes the parameters to this function in JSON Schema Object format. Reflects the Open API 3.03 Parameter Object. Parameter names are case-sensitive. For a function with no parameters, this can be left unset. | +| [parameters](./ai.functiondeclaration.md#functiondeclarationparameters) | [ObjectSchema](./ai.objectschema.md#objectschema_class) \| [ObjectSchemaRequest](./ai.objectschemarequest.md#objectschemarequest_interface) | Optional. Describes the parameters to this function in JSON Schema Object format. Reflects the Open API 3.03 Parameter Object. Parameter names are case-sensitive. For a function with no parameters, this can be left unset. | ## FunctionDeclaration.description @@ -53,5 +53,5 @@ Optional. Describes the parameters to this function in JSON Schema Object format Signature: ```typescript -parameters?: ObjectSchemaInterface; +parameters?: ObjectSchema | ObjectSchemaRequest; ``` diff --git a/docs-devsite/ai.functiondeclarationstool.md b/docs-devsite/ai.functiondeclarationstool.md index bde785d730b..d72d9db2f53 100644 --- a/docs-devsite/ai.functiondeclarationstool.md +++ b/docs-devsite/ai.functiondeclarationstool.md @@ -15,7 +15,7 @@ A `FunctionDeclarationsTool` is a piece of code that enables the system to inter Signature: ```typescript -export declare interface FunctionDeclarationsTool +export interface FunctionDeclarationsTool ``` ## Properties diff --git a/docs-devsite/ai.functionresponse.md b/docs-devsite/ai.functionresponse.md index e0838cf515a..980d964f703 100644 --- a/docs-devsite/ai.functionresponse.md +++ b/docs-devsite/ai.functionresponse.md @@ -22,9 +22,22 @@ export interface FunctionResponse | Property | Type | Description | | --- | --- | --- | +| [id](./ai.functionresponse.md#functionresponseid) | string | The id of the [FunctionCall](./ai.functioncall.md#functioncall_interface). | | [name](./ai.functionresponse.md#functionresponsename) | string | | | [response](./ai.functionresponse.md#functionresponseresponse) | object | | +## FunctionResponse.id + +The id of the [FunctionCall](./ai.functioncall.md#functioncall_interface). + +This property is only supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). When using the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)), this property will be `undefined`. + +Signature: + +```typescript +id?: string; +``` + ## FunctionResponse.name Signature: diff --git a/docs-devsite/ai.functionresponsepart.md b/docs-devsite/ai.functionresponsepart.md index 9c80258f43f..2d80e1706a3 100644 --- a/docs-devsite/ai.functionresponsepart.md +++ b/docs-devsite/ai.functionresponsepart.md @@ -22,10 +22,29 @@ export interface FunctionResponsePart | Property | Type | Description | | --- | --- | --- | +| [codeExecutionResult](./ai.functionresponsepart.md#functionresponsepartcodeexecutionresult) | never | | +| [executableCode](./ai.functionresponsepart.md#functionresponsepartexecutablecode) | never | | | [functionCall](./ai.functionresponsepart.md#functionresponsepartfunctioncall) | never | | | [functionResponse](./ai.functionresponsepart.md#functionresponsepartfunctionresponse) | [FunctionResponse](./ai.functionresponse.md#functionresponse_interface) | | | [inlineData](./ai.functionresponsepart.md#functionresponsepartinlinedata) | never | | | [text](./ai.functionresponsepart.md#functionresponseparttext) | never | | +| [thought](./ai.functionresponsepart.md#functionresponsepartthought) | boolean | | + +## FunctionResponsePart.codeExecutionResult + +Signature: + +```typescript +codeExecutionResult?: never; +``` + +## FunctionResponsePart.executableCode + +Signature: + +```typescript +executableCode?: never; +``` ## FunctionResponsePart.functionCall @@ -58,3 +77,11 @@ inlineData?: never; ```typescript text?: never; ``` + +## FunctionResponsePart.thought + +Signature: + +```typescript +thought?: boolean; +``` diff --git a/docs-devsite/ai.generatecontentcandidate.md b/docs-devsite/ai.generatecontentcandidate.md index ca0383549a7..1691442ecfa 100644 --- a/docs-devsite/ai.generatecontentcandidate.md +++ b/docs-devsite/ai.generatecontentcandidate.md @@ -29,6 +29,7 @@ export interface GenerateContentCandidate | [groundingMetadata](./ai.generatecontentcandidate.md#generatecontentcandidategroundingmetadata) | [GroundingMetadata](./ai.groundingmetadata.md#groundingmetadata_interface) | | | [index](./ai.generatecontentcandidate.md#generatecontentcandidateindex) | number | | | [safetyRatings](./ai.generatecontentcandidate.md#generatecontentcandidatesafetyratings) | [SafetyRating](./ai.safetyrating.md#safetyrating_interface)\[\] | | +| [urlContextMetadata](./ai.generatecontentcandidate.md#generatecontentcandidateurlcontextmetadata) | [URLContextMetadata](./ai.urlcontextmetadata.md#urlcontextmetadata_interface) | | ## GenerateContentCandidate.citationMetadata @@ -85,3 +86,11 @@ index: number; ```typescript safetyRatings?: SafetyRating[]; ``` + +## GenerateContentCandidate.urlContextMetadata + +Signature: + +```typescript +urlContextMetadata?: URLContextMetadata; +``` diff --git a/docs-devsite/ai.generationconfig.md b/docs-devsite/ai.generationconfig.md index f9697a07454..7330b8e6993 100644 --- a/docs-devsite/ai.generationconfig.md +++ b/docs-devsite/ai.generationconfig.md @@ -28,9 +28,10 @@ export interface GenerationConfig | [presencePenalty](./ai.generationconfig.md#generationconfigpresencepenalty) | number | | | [responseMimeType](./ai.generationconfig.md#generationconfigresponsemimetype) | string | Output response MIME type of the generated candidate text. Supported MIME types are text/plain (default, text output), application/json (JSON response in the candidates), and text/x.enum. | | [responseModalities](./ai.generationconfig.md#generationconfigresponsemodalities) | [ResponseModality](./ai.md#responsemodality)\[\] | (Public Preview) Generation modalities to be returned in generation responses. | -| [responseSchema](./ai.generationconfig.md#generationconfigresponseschema) | [TypedSchema](./ai.md#typedschema) \| [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) | Output response schema of the generated candidate text. This value can be a class generated with a [Schema](./ai.schema.md#schema_class) static method like Schema.string() or Schema.object() or it can be a plain JS object matching the [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) interface.
Note: This only applies when the specified responseMIMEType supports a schema; currently this is limited to application/json and text/x.enum. | +| [responseSchema](./ai.generationconfig.md#generationconfigresponseschema) | [TypedSchema](./ai.md#typedschema) \| [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) | Output response schema of the generated candidate text. This value can be a class generated with a [Schema](./ai.schema.md#schema_class) static method like Schema.string() or Schema.object() or it can be a plain JS object matching the [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) interface.
Note: This only applies when the specified responseMimeType supports a schema; currently this is limited to application/json and text/x.enum. | | [stopSequences](./ai.generationconfig.md#generationconfigstopsequences) | string\[\] | | | [temperature](./ai.generationconfig.md#generationconfigtemperature) | number | | +| [thinkingConfig](./ai.generationconfig.md#generationconfigthinkingconfig) | [ThinkingConfig](./ai.thinkingconfig.md#thinkingconfig_interface) | Configuration for "thinking" behavior of compatible Gemini models. | | [topK](./ai.generationconfig.md#generationconfigtopk) | number | | | [topP](./ai.generationconfig.md#generationconfigtopp) | number | | @@ -93,7 +94,7 @@ responseModalities?: ResponseModality[]; ## GenerationConfig.responseSchema -Output response schema of the generated candidate text. This value can be a class generated with a [Schema](./ai.schema.md#schema_class) static method like `Schema.string()` or `Schema.object()` or it can be a plain JS object matching the [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) interface.
Note: This only applies when the specified `responseMIMEType` supports a schema; currently this is limited to `application/json` and `text/x.enum`. +Output response schema of the generated candidate text. This value can be a class generated with a [Schema](./ai.schema.md#schema_class) static method like `Schema.string()` or `Schema.object()` or it can be a plain JS object matching the [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) interface.
Note: This only applies when the specified `responseMimeType` supports a schema; currently this is limited to `application/json` and `text/x.enum`. Signature: @@ -117,6 +118,16 @@ stopSequences?: string[]; temperature?: number; ``` +## GenerationConfig.thinkingConfig + +Configuration for "thinking" behavior of compatible Gemini models. + +Signature: + +```typescript +thinkingConfig?: ThinkingConfig; +``` + ## GenerationConfig.topK Signature: diff --git a/docs-devsite/ai.generativemodel.md b/docs-devsite/ai.generativemodel.md index d91cf80e881..323fcfe9d76 100644 --- a/docs-devsite/ai.generativemodel.md +++ b/docs-devsite/ai.generativemodel.md @@ -23,7 +23,7 @@ export declare class GenerativeModel extends AIModel | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(ai, modelParams, requestOptions)](./ai.generativemodel.md#generativemodelconstructor) | | Constructs a new instance of the GenerativeModel class | +| [(constructor)(ai, modelParams, requestOptions, chromeAdapter)](./ai.generativemodel.md#generativemodelconstructor) | | Constructs a new instance of the GenerativeModel class | ## Properties @@ -52,7 +52,7 @@ Constructs a new instance of the `GenerativeModel` class Signature: ```typescript -constructor(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions); +constructor(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions, chromeAdapter?: ChromeAdapter | undefined); ``` #### Parameters @@ -62,6 +62,7 @@ constructor(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions); | ai | [AI](./ai.ai.md#ai_interface) | | | modelParams | [ModelParams](./ai.modelparams.md#modelparams_interface) | | | requestOptions | [RequestOptions](./ai.requestoptions.md#requestoptions_interface) | | +| chromeAdapter | [ChromeAdapter](./ai.chromeadapter.md#chromeadapter_interface) \| undefined | | ## GenerativeModel.generationConfig diff --git a/docs-devsite/ai.vertexaioptions.md b/docs-devsite/ai.googlesearch.md similarity index 51% rename from docs-devsite/ai.vertexaioptions.md rename to docs-devsite/ai.googlesearch.md index 311fa4785f7..78fdef51606 100644 --- a/docs-devsite/ai.vertexaioptions.md +++ b/docs-devsite/ai.googlesearch.md @@ -9,25 +9,13 @@ overwritten. Changes should be made in the source code at https://github.com/firebase/firebase-js-sdk {% endcomment %} -# VertexAIOptions interface -Options when initializing the Firebase AI SDK. +# GoogleSearch interface +Specifies the Google Search configuration. -Signature: - -```typescript -export interface VertexAIOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [location](./ai.vertexaioptions.md#vertexaioptionslocation) | string | | - -## VertexAIOptions.location +Currently, this is an empty object, but it's reserved for future configuration options. Signature: ```typescript -location?: string; +export interface GoogleSearch ``` diff --git a/docs-devsite/ai.googlesearchtool.md b/docs-devsite/ai.googlesearchtool.md new file mode 100644 index 00000000000..ef29ccbdaa0 --- /dev/null +++ b/docs-devsite/ai.googlesearchtool.md @@ -0,0 +1,39 @@ +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 %} + +# GoogleSearchTool interface +A tool that allows a Gemini model to connect to Google Search to access and incorporate up-to-date information from the web into its responses. + +Important: If using Grounding with Google Search, you are required to comply with the "Grounding with Google Search" usage requirements for your chosen API provider: [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) section within the Service Specific Terms). + +Signature: + +```typescript +export interface GoogleSearchTool +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [googleSearch](./ai.googlesearchtool.md#googlesearchtoolgooglesearch) | [GoogleSearch](./ai.googlesearch.md#googlesearch_interface) | Specifies the Google Search configuration. Currently, this is an empty object, but it's reserved for future configuration options.When using this feature, you are required to comply with the "Grounding with Google Search" usage requirements for your chosen API provider: [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) section within the Service Specific Terms). | + +## GoogleSearchTool.googleSearch + +Specifies the Google Search configuration. Currently, this is an empty object, but it's reserved for future configuration options. + +When using this feature, you are required to comply with the "Grounding with Google Search" usage requirements for your chosen API provider: [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) section within the Service Specific Terms). + +Signature: + +```typescript +googleSearch: GoogleSearch; +``` diff --git a/docs-devsite/ai.groundingattribution.md b/docs-devsite/ai.groundingattribution.md deleted file mode 100644 index a0895550bf1..00000000000 --- a/docs-devsite/ai.groundingattribution.md +++ /dev/null @@ -1,62 +0,0 @@ -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 %} - -# GroundingAttribution interface -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -export interface GroundingAttribution -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [confidenceScore](./ai.groundingattribution.md#groundingattributionconfidencescore) | number | | -| [retrievedContext](./ai.groundingattribution.md#groundingattributionretrievedcontext) | [RetrievedContextAttribution](./ai.retrievedcontextattribution.md#retrievedcontextattribution_interface) | | -| [segment](./ai.groundingattribution.md#groundingattributionsegment) | [Segment](./ai.segment.md#segment_interface) | | -| [web](./ai.groundingattribution.md#groundingattributionweb) | [WebAttribution](./ai.webattribution.md#webattribution_interface) | | - -## GroundingAttribution.confidenceScore - -Signature: - -```typescript -confidenceScore?: number; -``` - -## GroundingAttribution.retrievedContext - -Signature: - -```typescript -retrievedContext?: RetrievedContextAttribution; -``` - -## GroundingAttribution.segment - -Signature: - -```typescript -segment: Segment; -``` - -## GroundingAttribution.web - -Signature: - -```typescript -web?: WebAttribution; -``` diff --git a/docs-devsite/ai.groundingchunk.md b/docs-devsite/ai.groundingchunk.md new file mode 100644 index 00000000000..2b84af29d8e --- /dev/null +++ b/docs-devsite/ai.groundingchunk.md @@ -0,0 +1,35 @@ +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 %} + +# GroundingChunk interface +Represents a chunk of retrieved data that supports a claim in the model's response. This is part of the grounding information provided when grounding is enabled. + +Signature: + +```typescript +export interface GroundingChunk +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [web](./ai.groundingchunk.md#groundingchunkweb) | [WebGroundingChunk](./ai.webgroundingchunk.md#webgroundingchunk_interface) | Contains details if the grounding chunk is from a web source. | + +## GroundingChunk.web + +Contains details if the grounding chunk is from a web source. + +Signature: + +```typescript +web?: WebGroundingChunk; +``` diff --git a/docs-devsite/ai.groundingmetadata.md b/docs-devsite/ai.groundingmetadata.md index 90994d9c01c..3eaa42bfed3 100644 --- a/docs-devsite/ai.groundingmetadata.md +++ b/docs-devsite/ai.groundingmetadata.md @@ -10,7 +10,11 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # GroundingMetadata interface -Metadata returned to client when grounding is enabled. +Metadata returned when grounding is enabled. + +Currently, only Grounding with Google Search is supported (see [GoogleSearchTool](./ai.googlesearchtool.md#googlesearchtool_interface)). + +Important: If using Grounding with Google Search, you are required to comply with the "Grounding with Google Search" usage requirements for your chosen API provider: [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) section within the Service Specific Terms). Signature: @@ -22,32 +26,59 @@ export interface GroundingMetadata | Property | Type | Description | | --- | --- | --- | -| [groundingAttributions](./ai.groundingmetadata.md#groundingmetadatagroundingattributions) | [GroundingAttribution](./ai.groundingattribution.md#groundingattribution_interface)\[\] | | +| [groundingChunks](./ai.groundingmetadata.md#groundingmetadatagroundingchunks) | [GroundingChunk](./ai.groundingchunk.md#groundingchunk_interface)\[\] | A list of [GroundingChunk](./ai.groundingchunk.md#groundingchunk_interface) objects. Each chunk represents a piece of retrieved content (for example, from a web page). that the model used to ground its response. | +| [groundingSupports](./ai.groundingmetadata.md#groundingmetadatagroundingsupports) | [GroundingSupport](./ai.groundingsupport.md#groundingsupport_interface)\[\] | A list of [GroundingSupport](./ai.groundingsupport.md#groundingsupport_interface) objects. Each object details how specific segments of the model's response are supported by the groundingChunks. | | [retrievalQueries](./ai.groundingmetadata.md#groundingmetadataretrievalqueries) | string\[\] | | -| [webSearchQueries](./ai.groundingmetadata.md#groundingmetadatawebsearchqueries) | string\[\] | | +| [searchEntryPoint](./ai.groundingmetadata.md#groundingmetadatasearchentrypoint) | [SearchEntrypoint](./ai.searchentrypoint.md#searchentrypoint_interface) | Google Search entry point for web searches. This contains an HTML/CSS snippet that must be embedded in an app to display a Google Search entry point for follow-up web searches related to a model's "Grounded Response". | +| [webSearchQueries](./ai.groundingmetadata.md#groundingmetadatawebsearchqueries) | string\[\] | A list of web search queries that the model performed to gather the grounding information. These can be used to allow users to explore the search results themselves. | + +## GroundingMetadata.groundingChunks + +A list of [GroundingChunk](./ai.groundingchunk.md#groundingchunk_interface) objects. Each chunk represents a piece of retrieved content (for example, from a web page). that the model used to ground its response. + +Signature: + +```typescript +groundingChunks?: GroundingChunk[]; +``` + +## GroundingMetadata.groundingSupports + +A list of [GroundingSupport](./ai.groundingsupport.md#groundingsupport_interface) objects. Each object details how specific segments of the model's response are supported by the `groundingChunks`. -## GroundingMetadata.groundingAttributions +Signature: + +```typescript +groundingSupports?: GroundingSupport[]; +``` + +## GroundingMetadata.retrievalQueries > Warning: This API is now obsolete. > +> Use [GroundingSupport](./ai.groundingsupport.md#groundingsupport_interface) instead. > Signature: ```typescript -groundingAttributions: GroundingAttribution[]; +retrievalQueries?: string[]; ``` -## GroundingMetadata.retrievalQueries +## GroundingMetadata.searchEntryPoint + +Google Search entry point for web searches. This contains an HTML/CSS snippet that must be embedded in an app to display a Google Search entry point for follow-up web searches related to a model's "Grounded Response". Signature: ```typescript -retrievalQueries?: string[]; +searchEntryPoint?: SearchEntrypoint; ``` ## GroundingMetadata.webSearchQueries +A list of web search queries that the model performed to gather the grounding information. These can be used to allow users to explore the search results themselves. + Signature: ```typescript diff --git a/docs-devsite/ai.groundingsupport.md b/docs-devsite/ai.groundingsupport.md new file mode 100644 index 00000000000..67eb190497c --- /dev/null +++ b/docs-devsite/ai.groundingsupport.md @@ -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 %} + +# GroundingSupport interface +Provides information about how a specific segment of the model's response is supported by the retrieved grounding chunks. + +Signature: + +```typescript +export interface GroundingSupport +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [groundingChunkIndices](./ai.groundingsupport.md#groundingsupportgroundingchunkindices) | number\[\] | A list of indices that refer to specific [GroundingChunk](./ai.groundingchunk.md#groundingchunk_interface) objects within the [GroundingMetadata.groundingChunks](./ai.groundingmetadata.md#groundingmetadatagroundingchunks) array. These referenced chunks are the sources that support the claim made in the associated segment of the response. For example, an array [1, 3, 4] means that groundingChunks[1], groundingChunks[3], and groundingChunks[4] are the retrieved content supporting this part of the response. | +| [segment](./ai.groundingsupport.md#groundingsupportsegment) | [Segment](./ai.segment.md#segment_interface) | Specifies the segment of the model's response content that this grounding support pertains to. | + +## GroundingSupport.groundingChunkIndices + +A list of indices that refer to specific [GroundingChunk](./ai.groundingchunk.md#groundingchunk_interface) objects within the [GroundingMetadata.groundingChunks](./ai.groundingmetadata.md#groundingmetadatagroundingchunks) array. These referenced chunks are the sources that support the claim made in the associated `segment` of the response. For example, an array `[1, 3, 4]` means that `groundingChunks[1]`, `groundingChunks[3]`, and `groundingChunks[4]` are the retrieved content supporting this part of the response. + +Signature: + +```typescript +groundingChunkIndices?: number[]; +``` + +## GroundingSupport.segment + +Specifies the segment of the model's response content that this grounding support pertains to. + +Signature: + +```typescript +segment?: Segment; +``` diff --git a/docs-devsite/ai.hybridparams.md b/docs-devsite/ai.hybridparams.md new file mode 100644 index 00000000000..558b54abf8d --- /dev/null +++ b/docs-devsite/ai.hybridparams.md @@ -0,0 +1,69 @@ +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 %} + +# HybridParams interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configures hybrid inference. + +Signature: + +```typescript +export interface HybridParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [inCloudParams](./ai.hybridparams.md#hybridparamsincloudparams) | [ModelParams](./ai.modelparams.md#modelparams_interface) | (Public Preview) Optional. Specifies advanced params for in-cloud inference. | +| [mode](./ai.hybridparams.md#hybridparamsmode) | [InferenceMode](./ai.md#inferencemode) | (Public Preview) Specifies on-device or in-cloud inference. Defaults to prefer on-device. | +| [onDeviceParams](./ai.hybridparams.md#hybridparamsondeviceparams) | [OnDeviceParams](./ai.ondeviceparams.md#ondeviceparams_interface) | (Public Preview) Optional. Specifies advanced params for on-device inference. | + +## HybridParams.inCloudParams + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Optional. Specifies advanced params for in-cloud inference. + +Signature: + +```typescript +inCloudParams?: ModelParams; +``` + +## HybridParams.mode + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Specifies on-device or in-cloud inference. Defaults to prefer on-device. + +Signature: + +```typescript +mode: InferenceMode; +``` + +## HybridParams.onDeviceParams + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Optional. Specifies advanced params for on-device inference. + +Signature: + +```typescript +onDeviceParams?: OnDeviceParams; +``` diff --git a/docs-devsite/ai.imagengenerationconfig.md b/docs-devsite/ai.imagengenerationconfig.md index d4f32a7e5a3..55579045777 100644 --- a/docs-devsite/ai.imagengenerationconfig.md +++ b/docs-devsite/ai.imagengenerationconfig.md @@ -10,9 +10,6 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ImagenGenerationConfig interface -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Configuration options for generating images with Imagen. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images-imagen) for more details. @@ -27,17 +24,14 @@ export interface ImagenGenerationConfig | Property | Type | Description | | --- | --- | --- | -| [addWatermark](./ai.imagengenerationconfig.md#imagengenerationconfigaddwatermark) | boolean | (Public Preview) Whether to add an invisible watermark to generated images.If set to true, an invisible SynthID watermark is embedded in generated images to indicate that they are AI generated. If set to false, watermarking will be disabled.For Imagen 3 models, the default value is true; see the addWatermark documentation for more details.When using the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)), this will default to true, and cannot be turned off. | -| [aspectRatio](./ai.imagengenerationconfig.md#imagengenerationconfigaspectratio) | [ImagenAspectRatio](./ai.md#imagenaspectratio) | (Public Preview) The aspect ratio of the generated images. The default value is square 1:1. Supported aspect ratios depend on the Imagen model, see [ImagenAspectRatio](./ai.md#imagenaspectratio) for more details. | -| [imageFormat](./ai.imagengenerationconfig.md#imagengenerationconfigimageformat) | [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) | (Public Preview) The image format of the generated images. The default is PNG.See [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) for more details. | -| [negativePrompt](./ai.imagengenerationconfig.md#imagengenerationconfignegativeprompt) | string | (Public Preview) A description of what should be omitted from the generated images.Support for negative prompts depends on the Imagen model.See the [documentation](http://firebase.google.com/docs/vertex-ai/model-parameters#imagen) for more details.This is no longer supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)) in versions greater than imagen-3.0-generate-002. | -| [numberOfImages](./ai.imagengenerationconfig.md#imagengenerationconfignumberofimages) | number | (Public Preview) The number of images to generate. The default value is 1.The number of sample images that may be generated in each request depends on the model (typically up to 4); see the sampleCount documentation for more details. | +| [addWatermark](./ai.imagengenerationconfig.md#imagengenerationconfigaddwatermark) | boolean | Whether to add an invisible watermark to generated images.If set to true, an invisible SynthID watermark is embedded in generated images to indicate that they are AI generated. If set to false, watermarking will be disabled.For Imagen 3 models, the default value is true; see the addWatermark documentation for more details.When using the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)), this will default to true, and cannot be turned off. | +| [aspectRatio](./ai.imagengenerationconfig.md#imagengenerationconfigaspectratio) | [ImagenAspectRatio](./ai.md#imagenaspectratio) | The aspect ratio of the generated images. The default value is square 1:1. Supported aspect ratios depend on the Imagen model, see [ImagenAspectRatio](./ai.md#imagenaspectratio) for more details. | +| [imageFormat](./ai.imagengenerationconfig.md#imagengenerationconfigimageformat) | [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) | The image format of the generated images. The default is PNG.See [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) for more details. | +| [negativePrompt](./ai.imagengenerationconfig.md#imagengenerationconfignegativeprompt) | string | A description of what should be omitted from the generated images.Support for negative prompts depends on the Imagen model.See the [documentation](http://firebase.google.com/docs/vertex-ai/model-parameters#imagen) for more details.This is no longer supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)) in versions greater than imagen-3.0-generate-002. | +| [numberOfImages](./ai.imagengenerationconfig.md#imagengenerationconfignumberofimages) | number | The number of images to generate. The default value is 1.The number of sample images that may be generated in each request depends on the model (typically up to 4); see the sampleCount documentation for more details. | ## ImagenGenerationConfig.addWatermark -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Whether to add an invisible watermark to generated images. If set to `true`, an invisible SynthID watermark is embedded in generated images to indicate that they are AI generated. If set to `false`, watermarking will be disabled. @@ -54,9 +48,6 @@ addWatermark?: boolean; ## ImagenGenerationConfig.aspectRatio -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The aspect ratio of the generated images. The default value is square 1:1. Supported aspect ratios depend on the Imagen model, see [ImagenAspectRatio](./ai.md#imagenaspectratio) for more details. Signature: @@ -67,9 +58,6 @@ aspectRatio?: ImagenAspectRatio; ## ImagenGenerationConfig.imageFormat -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The image format of the generated images. The default is PNG. See [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) for more details. @@ -82,9 +70,6 @@ imageFormat?: ImagenImageFormat; ## ImagenGenerationConfig.negativePrompt -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - A description of what should be omitted from the generated images. Support for negative prompts depends on the Imagen model. @@ -101,9 +86,6 @@ negativePrompt?: string; ## ImagenGenerationConfig.numberOfImages -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The number of images to generate. The default value is 1. The number of sample images that may be generated in each request depends on the model (typically up to 4); see the sampleCount documentation for more details. diff --git a/docs-devsite/ai.imagengenerationresponse.md b/docs-devsite/ai.imagengenerationresponse.md index 54b0ac9b1a9..033c966f099 100644 --- a/docs-devsite/ai.imagengenerationresponse.md +++ b/docs-devsite/ai.imagengenerationresponse.md @@ -10,9 +10,6 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ImagenGenerationResponse interface -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The response from a request to generate images with Imagen. Signature: @@ -25,14 +22,11 @@ export interface ImagenGenerationResponse(Public Preview) The reason that images were filtered out. This property will only be defined if one or more images were filtered.Images may be filtered out due to the [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel), [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel), or filtering included in the model. The filter levels may be adjusted in your [ImagenSafetySettings](./ai.imagensafetysettings.md#imagensafetysettings_interface).See the [Responsible AI and usage guidelines for Imagen](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen) for more details. | -| [images](./ai.imagengenerationresponse.md#imagengenerationresponseimages) | T\[\] | (Public Preview) The images generated by Imagen.The number of images generated may be fewer than the number requested if one or more were filtered out; see filteredReason. | +| [filteredReason](./ai.imagengenerationresponse.md#imagengenerationresponsefilteredreason) | string | The reason that images were filtered out. This property will only be defined if one or more images were filtered.Images may be filtered out due to the [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel), [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel), or filtering included in the model. The filter levels may be adjusted in your [ImagenSafetySettings](./ai.imagensafetysettings.md#imagensafetysettings_interface).See the [Responsible AI and usage guidelines for Imagen](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen) for more details. | +| [images](./ai.imagengenerationresponse.md#imagengenerationresponseimages) | T\[\] | The images generated by Imagen.The number of images generated may be fewer than the number requested if one or more were filtered out; see filteredReason. | ## ImagenGenerationResponse.filteredReason -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The reason that images were filtered out. This property will only be defined if one or more images were filtered. Images may be filtered out due to the [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel), [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel), or filtering included in the model. The filter levels may be adjusted in your [ImagenSafetySettings](./ai.imagensafetysettings.md#imagensafetysettings_interface). @@ -47,9 +41,6 @@ filteredReason?: string; ## ImagenGenerationResponse.images -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The images generated by Imagen. The number of images generated may be fewer than the number requested if one or more were filtered out; see `filteredReason`. diff --git a/docs-devsite/ai.imagenimageformat.md b/docs-devsite/ai.imagenimageformat.md index bd0bdf1baa7..df22d8266f7 100644 --- a/docs-devsite/ai.imagenimageformat.md +++ b/docs-devsite/ai.imagenimageformat.md @@ -10,9 +10,6 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ImagenImageFormat class -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Defines the image format for images generated by Imagen. Use this class to specify the desired format (JPEG or PNG) and compression quality for images generated by Imagen. This is typically included as part of [ImagenModelParams](./ai.imagenmodelparams.md#imagenmodelparams_interface). @@ -27,21 +24,18 @@ export declare class ImagenImageFormat | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [compressionQuality](./ai.imagenimageformat.md#imagenimageformatcompressionquality) | | number | (Public Preview) The level of compression (a number between 0 and 100). | -| [mimeType](./ai.imagenimageformat.md#imagenimageformatmimetype) | | string | (Public Preview) The MIME type. | +| [compressionQuality](./ai.imagenimageformat.md#imagenimageformatcompressionquality) | | number | The level of compression (a number between 0 and 100). | +| [mimeType](./ai.imagenimageformat.md#imagenimageformatmimetype) | | string | The MIME type. | ## Methods | Method | Modifiers | Description | | --- | --- | --- | -| [jpeg(compressionQuality)](./ai.imagenimageformat.md#imagenimageformatjpeg) | static | (Public Preview) Creates an [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) for a JPEG image. | -| [png()](./ai.imagenimageformat.md#imagenimageformatpng) | static | (Public Preview) Creates an [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) for a PNG image. | +| [jpeg(compressionQuality)](./ai.imagenimageformat.md#imagenimageformatjpeg) | static | Creates an [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) for a JPEG image. | +| [png()](./ai.imagenimageformat.md#imagenimageformatpng) | static | Creates an [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) for a PNG image. | ## ImagenImageFormat.compressionQuality -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The level of compression (a number between 0 and 100). Signature: @@ -52,9 +46,6 @@ compressionQuality?: number; ## ImagenImageFormat.mimeType -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The MIME type. Signature: @@ -65,9 +56,6 @@ mimeType: string; ## ImagenImageFormat.jpeg() -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Creates an [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) for a JPEG image. Signature: @@ -90,9 +78,6 @@ An [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) object ## ImagenImageFormat.png() -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Creates an [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) for a PNG image. Signature: diff --git a/docs-devsite/ai.imageninlineimage.md b/docs-devsite/ai.imageninlineimage.md index 4bb81cac55d..b2b541e301f 100644 --- a/docs-devsite/ai.imageninlineimage.md +++ b/docs-devsite/ai.imageninlineimage.md @@ -10,9 +10,6 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ImagenInlineImage interface -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - An image generated by Imagen, represented as inline data. Signature: @@ -25,14 +22,11 @@ export interface ImagenInlineImage | Property | Type | Description | | --- | --- | --- | -| [bytesBase64Encoded](./ai.imageninlineimage.md#imageninlineimagebytesbase64encoded) | string | (Public Preview) The base64-encoded image data. | -| [mimeType](./ai.imageninlineimage.md#imageninlineimagemimetype) | string | (Public Preview) The MIME type of the image; either "image/png" or "image/jpeg".To request a different format, set the imageFormat property in your [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface). | +| [bytesBase64Encoded](./ai.imageninlineimage.md#imageninlineimagebytesbase64encoded) | string | The base64-encoded image data. | +| [mimeType](./ai.imageninlineimage.md#imageninlineimagemimetype) | string | The MIME type of the image; either "image/png" or "image/jpeg".To request a different format, set the imageFormat property in your [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface). | ## ImagenInlineImage.bytesBase64Encoded -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The base64-encoded image data. Signature: @@ -43,9 +37,6 @@ bytesBase64Encoded: string; ## ImagenInlineImage.mimeType -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The MIME type of the image; either `"image/png"` or `"image/jpeg"`. To request a different format, set the `imageFormat` property in your [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface). diff --git a/docs-devsite/ai.imagenmodel.md b/docs-devsite/ai.imagenmodel.md index 911971e0988..68375972cbb 100644 --- a/docs-devsite/ai.imagenmodel.md +++ b/docs-devsite/ai.imagenmodel.md @@ -10,9 +10,6 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ImagenModel class -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Class for Imagen model APIs. This class provides methods for generating images using the Imagen model. @@ -28,27 +25,24 @@ export declare class ImagenModel extends AIModel | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(ai, modelParams, requestOptions)](./ai.imagenmodel.md#imagenmodelconstructor) | | (Public Preview) Constructs a new instance of the [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) class. | +| [(constructor)(ai, modelParams, requestOptions)](./ai.imagenmodel.md#imagenmodelconstructor) | | Constructs a new instance of the [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) class. | ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [generationConfig](./ai.imagenmodel.md#imagenmodelgenerationconfig) | | [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface) | (Public Preview) The Imagen generation configuration. | -| [requestOptions](./ai.imagenmodel.md#imagenmodelrequestoptions) | | [RequestOptions](./ai.requestoptions.md#requestoptions_interface) \| undefined | (Public Preview) | -| [safetySettings](./ai.imagenmodel.md#imagenmodelsafetysettings) | | [ImagenSafetySettings](./ai.imagensafetysettings.md#imagensafetysettings_interface) | (Public Preview) Safety settings for filtering inappropriate content. | +| [generationConfig](./ai.imagenmodel.md#imagenmodelgenerationconfig) | | [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface) | The Imagen generation configuration. | +| [requestOptions](./ai.imagenmodel.md#imagenmodelrequestoptions) | | [RequestOptions](./ai.requestoptions.md#requestoptions_interface) \| undefined | | +| [safetySettings](./ai.imagenmodel.md#imagenmodelsafetysettings) | | [ImagenSafetySettings](./ai.imagensafetysettings.md#imagensafetysettings_interface) | Safety settings for filtering inappropriate content. | ## Methods | Method | Modifiers | Description | | --- | --- | --- | -| [generateImages(prompt)](./ai.imagenmodel.md#imagenmodelgenerateimages) | | (Public Preview) Generates images using the Imagen model and returns them as base64-encoded strings. | +| [generateImages(prompt)](./ai.imagenmodel.md#imagenmodelgenerateimages) | | Generates images using the Imagen model and returns them as base64-encoded strings. | ## ImagenModel.(constructor) -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Constructs a new instance of the [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) class. Signature: @@ -71,9 +65,6 @@ If the `apiKey` or `projectId` fields are missing in your Firebase config. ## ImagenModel.generationConfig -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The Imagen generation configuration. Signature: @@ -84,9 +75,6 @@ generationConfig?: ImagenGenerationConfig; ## ImagenModel.requestOptions -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Signature: ```typescript @@ -95,9 +83,6 @@ requestOptions?: RequestOptions | undefined; ## ImagenModel.safetySettings -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Safety settings for filtering inappropriate content. Signature: @@ -108,9 +93,6 @@ safetySettings?: ImagenSafetySettings; ## ImagenModel.generateImages() -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Generates images using the Imagen model and returns them as base64-encoded strings. If the prompt was not blocked, but one or more of the generated images were filtered, the returned object will have a `filteredReason` property. If all images are filtered, the `images` array will be empty. diff --git a/docs-devsite/ai.imagenmodelparams.md b/docs-devsite/ai.imagenmodelparams.md index a63345b64e6..6d7566bc4d5 100644 --- a/docs-devsite/ai.imagenmodelparams.md +++ b/docs-devsite/ai.imagenmodelparams.md @@ -10,9 +10,6 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ImagenModelParams interface -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Parameters for configuring an [ImagenModel](./ai.imagenmodel.md#imagenmodel_class). Signature: @@ -25,15 +22,12 @@ export interface ImagenModelParams | Property | Type | Description | | --- | --- | --- | -| [generationConfig](./ai.imagenmodelparams.md#imagenmodelparamsgenerationconfig) | [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface) | (Public Preview) Configuration options for generating images with Imagen. | -| [model](./ai.imagenmodelparams.md#imagenmodelparamsmodel) | string | (Public Preview) The Imagen model to use for generating images. For example: imagen-3.0-generate-002.Only Imagen 3 models (named imagen-3.0-*) are supported.See [model versions](https://firebase.google.com/docs/vertex-ai/models) for a full list of supported Imagen 3 models. | -| [safetySettings](./ai.imagenmodelparams.md#imagenmodelparamssafetysettings) | [ImagenSafetySettings](./ai.imagensafetysettings.md#imagensafetysettings_interface) | (Public Preview) Safety settings for filtering potentially inappropriate content. | +| [generationConfig](./ai.imagenmodelparams.md#imagenmodelparamsgenerationconfig) | [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface) | Configuration options for generating images with Imagen. | +| [model](./ai.imagenmodelparams.md#imagenmodelparamsmodel) | string | The Imagen model to use for generating images. For example: imagen-3.0-generate-002.Only Imagen 3 models (named imagen-3.0-*) are supported.See [model versions](https://firebase.google.com/docs/vertex-ai/models) for a full list of supported Imagen 3 models. | +| [safetySettings](./ai.imagenmodelparams.md#imagenmodelparamssafetysettings) | [ImagenSafetySettings](./ai.imagensafetysettings.md#imagensafetysettings_interface) | Safety settings for filtering potentially inappropriate content. | ## ImagenModelParams.generationConfig -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Configuration options for generating images with Imagen. Signature: @@ -44,9 +38,6 @@ generationConfig?: ImagenGenerationConfig; ## ImagenModelParams.model -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - The Imagen model to use for generating images. For example: `imagen-3.0-generate-002`. Only Imagen 3 models (named `imagen-3.0-*`) are supported. @@ -61,9 +52,6 @@ model: string; ## ImagenModelParams.safetySettings -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Safety settings for filtering potentially inappropriate content. Signature: diff --git a/docs-devsite/ai.imagensafetysettings.md b/docs-devsite/ai.imagensafetysettings.md index 366e615d243..bc71f116d48 100644 --- a/docs-devsite/ai.imagensafetysettings.md +++ b/docs-devsite/ai.imagensafetysettings.md @@ -10,9 +10,6 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ImagenSafetySettings interface -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Settings for controlling the aggressiveness of filtering out sensitive content. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details. @@ -27,14 +24,11 @@ export interface ImagenSafetySettings | Property | Type | Description | | --- | --- | --- | -| [personFilterLevel](./ai.imagensafetysettings.md#imagensafetysettingspersonfilterlevel) | [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel) | (Public Preview) A filter level controlling whether generation of images containing people or faces is allowed. | -| [safetyFilterLevel](./ai.imagensafetysettings.md#imagensafetysettingssafetyfilterlevel) | [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel) | (Public Preview) A filter level controlling how aggressive to filter out sensitive content from generated images. | +| [personFilterLevel](./ai.imagensafetysettings.md#imagensafetysettingspersonfilterlevel) | [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel) | A filter level controlling whether generation of images containing people or faces is allowed. | +| [safetyFilterLevel](./ai.imagensafetysettings.md#imagensafetysettingssafetyfilterlevel) | [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel) | A filter level controlling how aggressive to filter out sensitive content from generated images. | ## ImagenSafetySettings.personFilterLevel -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - A filter level controlling whether generation of images containing people or faces is allowed. Signature: @@ -45,9 +39,6 @@ personFilterLevel?: ImagenPersonFilterLevel; ## ImagenSafetySettings.safetyFilterLevel -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - A filter level controlling how aggressive to filter out sensitive content from generated images. Signature: diff --git a/docs-devsite/ai.inlinedatapart.md b/docs-devsite/ai.inlinedatapart.md index 0dd68edda68..a3d581861e7 100644 --- a/docs-devsite/ai.inlinedatapart.md +++ b/docs-devsite/ai.inlinedatapart.md @@ -22,12 +22,31 @@ export interface InlineDataPart | Property | Type | Description | | --- | --- | --- | +| [codeExecutionResult](./ai.inlinedatapart.md#inlinedatapartcodeexecutionresult) | never | | +| [executableCode](./ai.inlinedatapart.md#inlinedatapartexecutablecode) | never | | | [functionCall](./ai.inlinedatapart.md#inlinedatapartfunctioncall) | never | | | [functionResponse](./ai.inlinedatapart.md#inlinedatapartfunctionresponse) | never | | | [inlineData](./ai.inlinedatapart.md#inlinedatapartinlinedata) | [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface) | | | [text](./ai.inlinedatapart.md#inlinedataparttext) | never | | +| [thought](./ai.inlinedatapart.md#inlinedatapartthought) | boolean | | | [videoMetadata](./ai.inlinedatapart.md#inlinedatapartvideometadata) | [VideoMetadata](./ai.videometadata.md#videometadata_interface) | Applicable if inlineData is a video. | +## InlineDataPart.codeExecutionResult + +Signature: + +```typescript +codeExecutionResult?: never; +``` + +## InlineDataPart.executableCode + +Signature: + +```typescript +executableCode?: never; +``` + ## InlineDataPart.functionCall Signature: @@ -60,6 +79,14 @@ inlineData: GenerativeContentBlob; text?: never; ``` +## InlineDataPart.thought + +Signature: + +```typescript +thought?: boolean; +``` + ## InlineDataPart.videoMetadata Applicable if `inlineData` is a video. diff --git a/docs-devsite/ai.languagemodelcreatecoreoptions.md b/docs-devsite/ai.languagemodelcreatecoreoptions.md new file mode 100644 index 00000000000..299d5d10603 --- /dev/null +++ b/docs-devsite/ai.languagemodelcreatecoreoptions.md @@ -0,0 +1,63 @@ +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 %} + +# LanguageModelCreateCoreOptions interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configures the creation of an on-device language model session. + +Signature: + +```typescript +export interface LanguageModelCreateCoreOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [expectedInputs](./ai.languagemodelcreatecoreoptions.md#languagemodelcreatecoreoptionsexpectedinputs) | [LanguageModelExpected](./ai.languagemodelexpected.md#languagemodelexpected_interface)\[\] | (Public Preview) | +| [temperature](./ai.languagemodelcreatecoreoptions.md#languagemodelcreatecoreoptionstemperature) | number | (Public Preview) | +| [topK](./ai.languagemodelcreatecoreoptions.md#languagemodelcreatecoreoptionstopk) | number | (Public Preview) | + +## LanguageModelCreateCoreOptions.expectedInputs + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +expectedInputs?: LanguageModelExpected[]; +``` + +## LanguageModelCreateCoreOptions.temperature + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +temperature?: number; +``` + +## LanguageModelCreateCoreOptions.topK + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +topK?: number; +``` diff --git a/docs-devsite/ai.languagemodelcreateoptions.md b/docs-devsite/ai.languagemodelcreateoptions.md new file mode 100644 index 00000000000..5949722d7e3 --- /dev/null +++ b/docs-devsite/ai.languagemodelcreateoptions.md @@ -0,0 +1,52 @@ +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 %} + +# LanguageModelCreateOptions interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configures the creation of an on-device language model session. + +Signature: + +```typescript +export interface LanguageModelCreateOptions extends LanguageModelCreateCoreOptions +``` +Extends: [LanguageModelCreateCoreOptions](./ai.languagemodelcreatecoreoptions.md#languagemodelcreatecoreoptions_interface) + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [initialPrompts](./ai.languagemodelcreateoptions.md#languagemodelcreateoptionsinitialprompts) | [LanguageModelMessage](./ai.languagemodelmessage.md#languagemodelmessage_interface)\[\] | (Public Preview) | +| [signal](./ai.languagemodelcreateoptions.md#languagemodelcreateoptionssignal) | AbortSignal | (Public Preview) | + +## LanguageModelCreateOptions.initialPrompts + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +initialPrompts?: LanguageModelMessage[]; +``` + +## LanguageModelCreateOptions.signal + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +signal?: AbortSignal; +``` diff --git a/docs-devsite/ai.languagemodelexpected.md b/docs-devsite/ai.languagemodelexpected.md new file mode 100644 index 00000000000..1afe4f86cc0 --- /dev/null +++ b/docs-devsite/ai.languagemodelexpected.md @@ -0,0 +1,51 @@ +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 %} + +# LanguageModelExpected interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Options for the expected inputs for an on-device language model. + +Signature: + +```typescript +export interface LanguageModelExpected +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [languages](./ai.languagemodelexpected.md#languagemodelexpectedlanguages) | string\[\] | (Public Preview) | +| [type](./ai.languagemodelexpected.md#languagemodelexpectedtype) | [LanguageModelMessageType](./ai.md#languagemodelmessagetype) | (Public Preview) | + +## LanguageModelExpected.languages + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +languages?: string[]; +``` + +## LanguageModelExpected.type + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +type: LanguageModelMessageType; +``` diff --git a/docs-devsite/ai.languagemodelmessage.md b/docs-devsite/ai.languagemodelmessage.md new file mode 100644 index 00000000000..5f133e458bc --- /dev/null +++ b/docs-devsite/ai.languagemodelmessage.md @@ -0,0 +1,51 @@ +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 %} + +# LanguageModelMessage interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +An on-device language model message. + +Signature: + +```typescript +export interface LanguageModelMessage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [content](./ai.languagemodelmessage.md#languagemodelmessagecontent) | [LanguageModelMessageContent](./ai.languagemodelmessagecontent.md#languagemodelmessagecontent_interface)\[\] | (Public Preview) | +| [role](./ai.languagemodelmessage.md#languagemodelmessagerole) | [LanguageModelMessageRole](./ai.md#languagemodelmessagerole) | (Public Preview) | + +## LanguageModelMessage.content + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +content: LanguageModelMessageContent[]; +``` + +## LanguageModelMessage.role + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +role: LanguageModelMessageRole; +``` diff --git a/docs-devsite/ai.languagemodelmessagecontent.md b/docs-devsite/ai.languagemodelmessagecontent.md new file mode 100644 index 00000000000..0545882c983 --- /dev/null +++ b/docs-devsite/ai.languagemodelmessagecontent.md @@ -0,0 +1,51 @@ +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 %} + +# LanguageModelMessageContent interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +An on-device language model content object. + +Signature: + +```typescript +export interface LanguageModelMessageContent +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./ai.languagemodelmessagecontent.md#languagemodelmessagecontenttype) | [LanguageModelMessageType](./ai.md#languagemodelmessagetype) | (Public Preview) | +| [value](./ai.languagemodelmessagecontent.md#languagemodelmessagecontentvalue) | [LanguageModelMessageContentValue](./ai.md#languagemodelmessagecontentvalue) | (Public Preview) | + +## LanguageModelMessageContent.type + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +type: LanguageModelMessageType; +``` + +## LanguageModelMessageContent.value + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +value: LanguageModelMessageContentValue; +``` diff --git a/docs-devsite/ai.languagemodelpromptoptions.md b/docs-devsite/ai.languagemodelpromptoptions.md new file mode 100644 index 00000000000..d681fdec94f --- /dev/null +++ b/docs-devsite/ai.languagemodelpromptoptions.md @@ -0,0 +1,39 @@ +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 %} + +# LanguageModelPromptOptions interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Options for an on-device language model prompt. + +Signature: + +```typescript +export interface LanguageModelPromptOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [responseConstraint](./ai.languagemodelpromptoptions.md#languagemodelpromptoptionsresponseconstraint) | object | (Public Preview) | + +## LanguageModelPromptOptions.responseConstraint + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +responseConstraint?: object; +``` diff --git a/docs-devsite/ai.livegenerationconfig.md b/docs-devsite/ai.livegenerationconfig.md new file mode 100644 index 00000000000..1a920afa1e7 --- /dev/null +++ b/docs-devsite/ai.livegenerationconfig.md @@ -0,0 +1,139 @@ +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 %} + +# LiveGenerationConfig interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configuration parameters used by [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) to control live content generation. + +Signature: + +```typescript +export interface LiveGenerationConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [frequencyPenalty](./ai.livegenerationconfig.md#livegenerationconfigfrequencypenalty) | number | (Public Preview) Frequency penalties. | +| [maxOutputTokens](./ai.livegenerationconfig.md#livegenerationconfigmaxoutputtokens) | number | (Public Preview) Specifies the maximum number of tokens that can be generated in the response. The number of tokens per word varies depending on the language outputted. Is unbounded by default. | +| [presencePenalty](./ai.livegenerationconfig.md#livegenerationconfigpresencepenalty) | number | (Public Preview) Positive penalties. | +| [responseModalities](./ai.livegenerationconfig.md#livegenerationconfigresponsemodalities) | [ResponseModality](./ai.md#responsemodality)\[\] | (Public Preview) The modalities of the response. | +| [speechConfig](./ai.livegenerationconfig.md#livegenerationconfigspeechconfig) | [SpeechConfig](./ai.speechconfig.md#speechconfig_interface) | (Public Preview) Configuration for speech synthesis. | +| [temperature](./ai.livegenerationconfig.md#livegenerationconfigtemperature) | number | (Public Preview) Controls the degree of randomness in token selection. A temperature value of 0 means that the highest probability tokens are always selected. In this case, responses for a given prompt are mostly deterministic, but a small amount of variation is still possible. | +| [topK](./ai.livegenerationconfig.md#livegenerationconfigtopk) | number | (Public Preview) Changes how the model selects token for output. A topK value of 1 means the select token is the most probable among all tokens in the model's vocabulary, while a topK value 3 means that the next token is selected from among the 3 most probably using probabilities sampled. Tokens are then further filtered with the highest selected temperature sampling. Defaults to 40 if unspecified. | +| [topP](./ai.livegenerationconfig.md#livegenerationconfigtopp) | number | (Public Preview) Changes how the model selects tokens for output. Tokens are selected from the most to least probable until the sum of their probabilities equals the topP value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively and the topP value is 0.5, then the model will select either A or B as the next token by using the temperature and exclude C as a candidate. Defaults to 0.95 if unset. | + +## LiveGenerationConfig.frequencyPenalty + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Frequency penalties. + +Signature: + +```typescript +frequencyPenalty?: number; +``` + +## LiveGenerationConfig.maxOutputTokens + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Specifies the maximum number of tokens that can be generated in the response. The number of tokens per word varies depending on the language outputted. Is unbounded by default. + +Signature: + +```typescript +maxOutputTokens?: number; +``` + +## LiveGenerationConfig.presencePenalty + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Positive penalties. + +Signature: + +```typescript +presencePenalty?: number; +``` + +## LiveGenerationConfig.responseModalities + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The modalities of the response. + +Signature: + +```typescript +responseModalities?: ResponseModality[]; +``` + +## LiveGenerationConfig.speechConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configuration for speech synthesis. + +Signature: + +```typescript +speechConfig?: SpeechConfig; +``` + +## LiveGenerationConfig.temperature + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Controls the degree of randomness in token selection. A `temperature` value of 0 means that the highest probability tokens are always selected. In this case, responses for a given prompt are mostly deterministic, but a small amount of variation is still possible. + +Signature: + +```typescript +temperature?: number; +``` + +## LiveGenerationConfig.topK + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Changes how the model selects token for output. A `topK` value of 1 means the select token is the most probable among all tokens in the model's vocabulary, while a `topK` value 3 means that the next token is selected from among the 3 most probably using probabilities sampled. Tokens are then further filtered with the highest selected `temperature` sampling. Defaults to 40 if unspecified. + +Signature: + +```typescript +topK?: number; +``` + +## LiveGenerationConfig.topP + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Changes how the model selects tokens for output. Tokens are selected from the most to least probable until the sum of their probabilities equals the `topP` value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively and the `topP` value is 0.5, then the model will select either A or B as the next token by using the `temperature` and exclude C as a candidate. Defaults to 0.95 if unset. + +Signature: + +```typescript +topP?: number; +``` diff --git a/docs-devsite/ai.livegenerativemodel.md b/docs-devsite/ai.livegenerativemodel.md new file mode 100644 index 00000000000..7c52cad1a33 --- /dev/null +++ b/docs-devsite/ai.livegenerativemodel.md @@ -0,0 +1,109 @@ +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 %} + +# LiveGenerativeModel class +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Class for Live generative model APIs. The Live API enables low-latency, two-way multimodal interactions with Gemini. + +This class should only be instantiated with [getLiveGenerativeModel()](./ai.md#getlivegenerativemodel_f2099ac). + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `LiveGenerativeModel` class. + +Signature: + +```typescript +export declare class LiveGenerativeModel extends AIModel +``` +Extends: [AIModel](./ai.aimodel.md#aimodel_class) + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [generationConfig](./ai.livegenerativemodel.md#livegenerativemodelgenerationconfig) | | [LiveGenerationConfig](./ai.livegenerationconfig.md#livegenerationconfig_interface) | (Public Preview) | +| [systemInstruction](./ai.livegenerativemodel.md#livegenerativemodelsysteminstruction) | | [Content](./ai.content.md#content_interface) | (Public Preview) | +| [toolConfig](./ai.livegenerativemodel.md#livegenerativemodeltoolconfig) | | [ToolConfig](./ai.toolconfig.md#toolconfig_interface) | (Public Preview) | +| [tools](./ai.livegenerativemodel.md#livegenerativemodeltools) | | [Tool](./ai.md#tool)\[\] | (Public Preview) | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [connect()](./ai.livegenerativemodel.md#livegenerativemodelconnect) | | (Public Preview) Starts a [LiveSession](./ai.livesession.md#livesession_class). | + +## LiveGenerativeModel.generationConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +generationConfig: LiveGenerationConfig; +``` + +## LiveGenerativeModel.systemInstruction + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +systemInstruction?: Content; +``` + +## LiveGenerativeModel.toolConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +toolConfig?: ToolConfig; +``` + +## LiveGenerativeModel.tools + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +tools?: Tool[]; +``` + +## LiveGenerativeModel.connect() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Starts a [LiveSession](./ai.livesession.md#livesession_class). + +Signature: + +```typescript +connect(): Promise; +``` +Returns: + +Promise<[LiveSession](./ai.livesession.md#livesession_class)> + +A [LiveSession](./ai.livesession.md#livesession_class). + +#### Exceptions + +If the connection failed to be established with the server. + diff --git a/docs-devsite/ai.livemodelparams.md b/docs-devsite/ai.livemodelparams.md new file mode 100644 index 00000000000..fddca4f0e14 --- /dev/null +++ b/docs-devsite/ai.livemodelparams.md @@ -0,0 +1,87 @@ +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 %} + +# LiveModelParams interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Params passed to [getLiveGenerativeModel()](./ai.md#getlivegenerativemodel_f2099ac). + +Signature: + +```typescript +export interface LiveModelParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [generationConfig](./ai.livemodelparams.md#livemodelparamsgenerationconfig) | [LiveGenerationConfig](./ai.livegenerationconfig.md#livegenerationconfig_interface) | (Public Preview) | +| [model](./ai.livemodelparams.md#livemodelparamsmodel) | string | (Public Preview) | +| [systemInstruction](./ai.livemodelparams.md#livemodelparamssysteminstruction) | string \| [Part](./ai.md#part) \| [Content](./ai.content.md#content_interface) | (Public Preview) | +| [toolConfig](./ai.livemodelparams.md#livemodelparamstoolconfig) | [ToolConfig](./ai.toolconfig.md#toolconfig_interface) | (Public Preview) | +| [tools](./ai.livemodelparams.md#livemodelparamstools) | [Tool](./ai.md#tool)\[\] | (Public Preview) | + +## LiveModelParams.generationConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +generationConfig?: LiveGenerationConfig; +``` + +## LiveModelParams.model + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +model: string; +``` + +## LiveModelParams.systemInstruction + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +systemInstruction?: string | Part | Content; +``` + +## LiveModelParams.toolConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +toolConfig?: ToolConfig; +``` + +## LiveModelParams.tools + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +tools?: Tool[]; +``` diff --git a/docs-devsite/ai.liveservercontent.md b/docs-devsite/ai.liveservercontent.md new file mode 100644 index 00000000000..f9c3ca1de79 --- /dev/null +++ b/docs-devsite/ai.liveservercontent.md @@ -0,0 +1,81 @@ +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 %} + +# LiveServerContent interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +An incremental content update from the model. + +Signature: + +```typescript +export interface LiveServerContent +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [interrupted](./ai.liveservercontent.md#liveservercontentinterrupted) | boolean | (Public Preview) Indicates whether the model was interrupted by the client. An interruption occurs when the client sends a message before the model finishes it's turn. This is undefined if the model was not interrupted. | +| [modelTurn](./ai.liveservercontent.md#liveservercontentmodelturn) | [Content](./ai.content.md#content_interface) | (Public Preview) The content that the model has generated as part of the current conversation with the user. | +| [turnComplete](./ai.liveservercontent.md#liveservercontentturncomplete) | boolean | (Public Preview) Indicates whether the turn is complete. This is undefined if the turn is not complete. | +| [type](./ai.liveservercontent.md#liveservercontenttype) | 'serverContent' | (Public Preview) | + +## LiveServerContent.interrupted + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether the model was interrupted by the client. An interruption occurs when the client sends a message before the model finishes it's turn. This is `undefined` if the model was not interrupted. + +Signature: + +```typescript +interrupted?: boolean; +``` + +## LiveServerContent.modelTurn + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The content that the model has generated as part of the current conversation with the user. + +Signature: + +```typescript +modelTurn?: Content; +``` + +## LiveServerContent.turnComplete + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether the turn is complete. This is `undefined` if the turn is not complete. + +Signature: + +```typescript +turnComplete?: boolean; +``` + +## LiveServerContent.type + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +type: 'serverContent'; +``` diff --git a/docs-devsite/ai.liveservertoolcall.md b/docs-devsite/ai.liveservertoolcall.md new file mode 100644 index 00000000000..51ef6bb5d4b --- /dev/null +++ b/docs-devsite/ai.liveservertoolcall.md @@ -0,0 +1,53 @@ +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 %} + +# LiveServerToolCall interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +A request from the model for the client to execute one or more functions. + +Signature: + +```typescript +export interface LiveServerToolCall +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [functionCalls](./ai.liveservertoolcall.md#liveservertoolcallfunctioncalls) | [FunctionCall](./ai.functioncall.md#functioncall_interface)\[\] | (Public Preview) An array of function calls to run. | +| [type](./ai.liveservertoolcall.md#liveservertoolcalltype) | 'toolCall' | (Public Preview) | + +## LiveServerToolCall.functionCalls + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +An array of function calls to run. + +Signature: + +```typescript +functionCalls: FunctionCall[]; +``` + +## LiveServerToolCall.type + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +type: 'toolCall'; +``` diff --git a/docs-devsite/ai.liveservertoolcallcancellation.md b/docs-devsite/ai.liveservertoolcallcancellation.md new file mode 100644 index 00000000000..2e9a63a81e7 --- /dev/null +++ b/docs-devsite/ai.liveservertoolcallcancellation.md @@ -0,0 +1,53 @@ +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 %} + +# LiveServerToolCallCancellation interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Notification to cancel a previous function call triggered by [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface). + +Signature: + +```typescript +export interface LiveServerToolCallCancellation +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [functionIds](./ai.liveservertoolcallcancellation.md#liveservertoolcallcancellationfunctionids) | string\[\] | (Public Preview) IDs of function calls that were cancelled. These refer to the id property of a [FunctionCall](./ai.functioncall.md#functioncall_interface). | +| [type](./ai.liveservertoolcallcancellation.md#liveservertoolcallcancellationtype) | 'toolCallCancellation' | (Public Preview) | + +## LiveServerToolCallCancellation.functionIds + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +IDs of function calls that were cancelled. These refer to the `id` property of a [FunctionCall](./ai.functioncall.md#functioncall_interface). + +Signature: + +```typescript +functionIds: string[]; +``` + +## LiveServerToolCallCancellation.type + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +type: 'toolCallCancellation'; +``` diff --git a/docs-devsite/ai.livesession.md b/docs-devsite/ai.livesession.md new file mode 100644 index 00000000000..558c5eb3bd6 --- /dev/null +++ b/docs-devsite/ai.livesession.md @@ -0,0 +1,218 @@ +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 %} + +# LiveSession class +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Represents an active, real-time, bidirectional conversation with the model. + +This class should only be instantiated by calling [LiveGenerativeModel.connect()](./ai.livegenerativemodel.md#livegenerativemodelconnect). + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `LiveSession` class. + +Signature: + +```typescript +export declare class LiveSession +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [inConversation](./ai.livesession.md#livesessioninconversation) | | boolean | (Public Preview) Indicates whether this Live session is being controlled by an AudioConversationController. | +| [isClosed](./ai.livesession.md#livesessionisclosed) | | boolean | (Public Preview) Indicates whether this Live session is closed. | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [close()](./ai.livesession.md#livesessionclose) | | (Public Preview) Closes this session. All methods on this session will throw an error once this resolves. | +| [receive()](./ai.livesession.md#livesessionreceive) | | (Public Preview) Yields messages received from the server. This can only be used by one consumer at a time. | +| [send(request, turnComplete)](./ai.livesession.md#livesessionsend) | | (Public Preview) Sends content to the server. | +| [sendFunctionResponses(functionResponses)](./ai.livesession.md#livesessionsendfunctionresponses) | | (Public Preview) Sends function responses to the server. | +| [sendMediaChunks(mediaChunks)](./ai.livesession.md#livesessionsendmediachunks) | | (Public Preview) Sends realtime input to the server. | +| [sendMediaStream(mediaChunkStream)](./ai.livesession.md#livesessionsendmediastream) | | (Public Preview) Sends a stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface). | + +## LiveSession.inConversation + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether this Live session is being controlled by an `AudioConversationController`. + +Signature: + +```typescript +inConversation: boolean; +``` + +## LiveSession.isClosed + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether this Live session is closed. + +Signature: + +```typescript +isClosed: boolean; +``` + +## LiveSession.close() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Closes this session. All methods on this session will throw an error once this resolves. + +Signature: + +```typescript +close(): Promise; +``` +Returns: + +Promise<void> + +## LiveSession.receive() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Yields messages received from the server. This can only be used by one consumer at a time. + +Signature: + +```typescript +receive(): AsyncGenerator; +``` +Returns: + +AsyncGenerator<[LiveServerContent](./ai.liveservercontent.md#liveservercontent_interface) \| [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface) \| [LiveServerToolCallCancellation](./ai.liveservertoolcallcancellation.md#liveservertoolcallcancellation_interface)> + +An `AsyncGenerator` that yields server messages as they arrive. + +#### Exceptions + +If the session is already closed, or if we receive a response that we don't support. + +## LiveSession.send() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends content to the server. + +Signature: + +```typescript +send(request: string | Array, turnComplete?: boolean): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| request | string \| Array<string \| [Part](./ai.md#part)> | The message to send to the model. | +| turnComplete | boolean | Indicates if the turn is complete. Defaults to false. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + +## LiveSession.sendFunctionResponses() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends function responses to the server. + +Signature: + +```typescript +sendFunctionResponses(functionResponses: FunctionResponse[]): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| functionResponses | [FunctionResponse](./ai.functionresponse.md#functionresponse_interface)\[\] | The function responses to send. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + +## LiveSession.sendMediaChunks() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends realtime input to the server. + +Signature: + +```typescript +sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| mediaChunks | [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface)\[\] | The media chunks to send. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + +## LiveSession.sendMediaStream() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends a stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface). + +Signature: + +```typescript +sendMediaStream(mediaChunkStream: ReadableStream): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| mediaChunkStream | ReadableStream<[GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface)> | The stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface) to send. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + diff --git a/docs-devsite/ai.md b/docs-devsite/ai.md index 286c8351fd7..db6148ee88c 100644 --- a/docs-devsite/ai.md +++ b/docs-devsite/ai.md @@ -18,10 +18,12 @@ The Firebase AI Web SDK. | --- | --- | | function(app, ...) | | [getAI(app, options)](./ai.md#getai_a94a413) | Returns the default [AI](./ai.ai.md#ai_interface) instance that is associated with the provided [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface). If no instance exists, initializes a new instance with the default settings. | -| [getVertexAI(app, options)](./ai.md#getvertexai_04094cf) | | | function(ai, ...) | -| [getGenerativeModel(ai, modelParams, requestOptions)](./ai.md#getgenerativemodel_80bd839) | Returns a [GenerativeModel](./ai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. | -| [getImagenModel(ai, modelParams, requestOptions)](./ai.md#getimagenmodel_e1f6645) | (Public Preview) Returns an [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen.Only Imagen 3 models (named imagen-3.0-*) are supported. | +| [getGenerativeModel(ai, modelParams, requestOptions)](./ai.md#getgenerativemodel_c63f46a) | Returns a [GenerativeModel](./ai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. | +| [getImagenModel(ai, modelParams, requestOptions)](./ai.md#getimagenmodel_e1f6645) | Returns an [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen.Only Imagen 3 models (named imagen-3.0-*) are supported. | +| [getLiveGenerativeModel(ai, modelParams)](./ai.md#getlivegenerativemodel_f2099ac) | (Public Preview) Returns a [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) class for real-time, bidirectional communication.The Live API is only supported in modern browser windows and Node >= 22. | +| function(liveSession, ...) | +| [startAudioConversation(liveSession, options)](./ai.md#startaudioconversation_01c8e7f) | (Public Preview) Starts a real-time, bidirectional audio conversation with the model. This helper function manages the complexities of microphone access, audio recording, playback, and interruptions. | ## Classes @@ -29,49 +31,38 @@ The Firebase AI Web SDK. | --- | --- | | [AIError](./ai.aierror.md#aierror_class) | Error class for the Firebase AI SDK. | | [AIModel](./ai.aimodel.md#aimodel_class) | Base class for Firebase AI model APIs.Instances of this class are associated with a specific Firebase AI [Backend](./ai.backend.md#backend_class) and provide methods for interacting with the configured generative model. | +| [AnyOfSchema](./ai.anyofschema.md#anyofschema_class) | Schema class representing a value that can conform to any of the provided sub-schemas. This is useful when a field can accept multiple distinct types or structures. | | [ArraySchema](./ai.arrayschema.md#arrayschema_class) | Schema class for "array" types. The items param should refer to the type of item that can be a member of the array. | | [Backend](./ai.backend.md#backend_class) | Abstract base class representing the configuration for an AI service backend. This class should not be instantiated directly. Use its subclasses; [GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class) for the Gemini Developer API (via [Google AI](https://ai.google/)), and [VertexAIBackend](./ai.vertexaibackend.md#vertexaibackend_class) for the Vertex AI Gemini API. | | [BooleanSchema](./ai.booleanschema.md#booleanschema_class) | Schema class for "boolean" types. | | [ChatSession](./ai.chatsession.md#chatsession_class) | ChatSession class that enables sending chat messages and stores history of sent and received messages so far. | | [GenerativeModel](./ai.generativemodel.md#generativemodel_class) | Class for generative model APIs. | | [GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class) | Configuration class for the Gemini Developer API.Use this with [AIOptions](./ai.aioptions.md#aioptions_interface) when initializing the AI service via [getAI()](./ai.md#getai_a94a413) to specify the Gemini Developer API as the backend. | -| [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) | (Public Preview) Defines the image format for images generated by Imagen.Use this class to specify the desired format (JPEG or PNG) and compression quality for images generated by Imagen. This is typically included as part of [ImagenModelParams](./ai.imagenmodelparams.md#imagenmodelparams_interface). | -| [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) | (Public Preview) Class for Imagen model APIs.This class provides methods for generating images using the Imagen model. | +| [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) | Defines the image format for images generated by Imagen.Use this class to specify the desired format (JPEG or PNG) and compression quality for images generated by Imagen. This is typically included as part of [ImagenModelParams](./ai.imagenmodelparams.md#imagenmodelparams_interface). | +| [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) | Class for Imagen model APIs.This class provides methods for generating images using the Imagen model. | | [IntegerSchema](./ai.integerschema.md#integerschema_class) | Schema class for "integer" types. | +| [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) | (Public Preview) Class for Live generative model APIs. The Live API enables low-latency, two-way multimodal interactions with Gemini.This class should only be instantiated with [getLiveGenerativeModel()](./ai.md#getlivegenerativemodel_f2099ac). | +| [LiveSession](./ai.livesession.md#livesession_class) | (Public Preview) Represents an active, real-time, bidirectional conversation with the model.This class should only be instantiated by calling [LiveGenerativeModel.connect()](./ai.livegenerativemodel.md#livegenerativemodelconnect). | | [NumberSchema](./ai.numberschema.md#numberschema_class) | Schema class for "number" types. | | [ObjectSchema](./ai.objectschema.md#objectschema_class) | Schema class for "object" types. The properties param must be a map of Schema objects. | | [Schema](./ai.schema.md#schema_class) | Parent class encompassing all Schema types, with static methods that allow building specific Schema types. This class can be converted with JSON.stringify() into a JSON string accepted by Vertex AI REST endpoints. (This string conversion is automatically done when calling SDK methods.) | | [StringSchema](./ai.stringschema.md#stringschema_class) | Schema class for "string" types. Can be used with or without enum values. | | [VertexAIBackend](./ai.vertexaibackend.md#vertexaibackend_class) | Configuration class for the Vertex AI Gemini API.Use this with [AIOptions](./ai.aioptions.md#aioptions_interface) when initializing the AI service via [getAI()](./ai.md#getai_a94a413) to specify the Vertex AI Gemini API as the backend. | -## Enumerations - -| Enumeration | Description | -| --- | --- | -| [AIErrorCode](./ai.md#aierrorcode) | Standardized error codes that [AIError](./ai.aierror.md#aierror_class) can have. | -| [BlockReason](./ai.md#blockreason) | Reason that a prompt was blocked. | -| [FinishReason](./ai.md#finishreason) | Reason that a candidate finished. | -| [FunctionCallingMode](./ai.md#functioncallingmode) | | -| [HarmBlockMethod](./ai.md#harmblockmethod) | This property is not supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). | -| [HarmBlockThreshold](./ai.md#harmblockthreshold) | Threshold above which a prompt or candidate will be blocked. | -| [HarmCategory](./ai.md#harmcategory) | Harm categories that would cause prompts or candidates to be blocked. | -| [HarmProbability](./ai.md#harmprobability) | Probability that a prompt or candidate matches a harm category. | -| [HarmSeverity](./ai.md#harmseverity) | Harm severity levels. | -| [ImagenAspectRatio](./ai.md#imagenaspectratio) | (Public Preview) Aspect ratios for Imagen images.To specify an aspect ratio for generated images, set the aspectRatio property in your [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface).See the the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. | -| [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel) | (Public Preview) A filter level controlling whether generation of images containing people or faces is allowed.See the personGeneration documentation for more details. | -| [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel) | (Public Preview) A filter level controlling how aggressively to filter sensitive content.Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, violence, sexual, derogatory, and toxic). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. | -| [Modality](./ai.md#modality) | Content part modality. | -| [SchemaType](./ai.md#schematype) | Contains the list of OpenAPI data types as defined by the [OpenAPI specification](https://swagger.io/docs/specification/data-models/data-types/) | - ## Interfaces | Interface | Description | | --- | --- | | [AI](./ai.ai.md#ai_interface) | An instance of the Firebase AI SDK.Do not create this instance directly. Instead, use [getAI()](./ai.md#getai_a94a413). | | [AIOptions](./ai.aioptions.md#aioptions_interface) | Options for initializing the AI service using [getAI()](./ai.md#getai_a94a413). This allows specifying which backend to use (Vertex AI Gemini API or Gemini Developer API) and configuring its specific options (like location for Vertex AI). | +| [AudioConversationController](./ai.audioconversationcontroller.md#audioconversationcontroller_interface) | (Public Preview) A controller for managing an active audio conversation. | | [BaseParams](./ai.baseparams.md#baseparams_interface) | Base parameters for a number of methods. | +| [ChromeAdapter](./ai.chromeadapter.md#chromeadapter_interface) | (Public Preview) Defines an inference "backend" that uses Chrome's on-device model, and encapsulates logic for detecting when on-device inference is possible.These methods should not be called directly by the user. | | [Citation](./ai.citation.md#citation_interface) | A single citation. | | [CitationMetadata](./ai.citationmetadata.md#citationmetadata_interface) | Citation metadata that may be found on a [GenerateContentCandidate](./ai.generatecontentcandidate.md#generatecontentcandidate_interface). | +| [CodeExecutionResult](./ai.codeexecutionresult.md#codeexecutionresult_interface) | (Public Preview) The results of code execution run by the model. | +| [CodeExecutionResultPart](./ai.codeexecutionresultpart.md#codeexecutionresultpart_interface) | (Public Preview) Represents the code execution result from the model. | +| [CodeExecutionTool](./ai.codeexecutiontool.md#codeexecutiontool_interface) | (Public Preview) A tool that enables the model to use code execution. | | [Content](./ai.content.md#content_interface) | Content type for both prompts and response candidates. | | [CountTokensRequest](./ai.counttokensrequest.md#counttokensrequest_interface) | Params for calling [GenerativeModel.countTokens()](./ai.generativemodel.md#generativemodelcounttokens) | | [CountTokensResponse](./ai.counttokensresponse.md#counttokensresponse_interface) | Response from calling [GenerativeModel.countTokens()](./ai.generativemodel.md#generativemodelcounttokens). | @@ -79,6 +70,8 @@ The Firebase AI Web SDK. | [Date\_2](./ai.date_2.md#date_2_interface) | Protobuf google.type.Date | | [EnhancedGenerateContentResponse](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponse_interface) | Response object wrapped with helper methods. | | [ErrorDetails](./ai.errordetails.md#errordetails_interface) | Details object that may be included in an error response. | +| [ExecutableCode](./ai.executablecode.md#executablecode_interface) | (Public Preview) An interface for executable code returned by the model. | +| [ExecutableCodePart](./ai.executablecodepart.md#executablecodepart_interface) | (Public Preview) Represents the code that is executed by the model. | | [FileData](./ai.filedata.md#filedata_interface) | Data pointing to a file uploaded on Google Cloud Storage. | | [FileDataPart](./ai.filedatapart.md#filedatapart_interface) | Content part interface if the part represents [FileData](./ai.filedata.md#filedata_interface) | | [FunctionCall](./ai.functioncall.md#functioncall_interface) | A predicted [FunctionCall](./ai.functioncall.md#functioncall_interface) returned from the model that contains a string representing the [FunctionDeclaration.name](./ai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing the parameters and their values. | @@ -95,20 +88,37 @@ The Firebase AI Web SDK. | [GenerateContentStreamResult](./ai.generatecontentstreamresult.md#generatecontentstreamresult_interface) | Result object returned from [GenerativeModel.generateContentStream()](./ai.generativemodel.md#generativemodelgeneratecontentstream) call. Iterate over stream to get chunks as they come in and/or use the response promise to get the aggregated response when the stream is done. | | [GenerationConfig](./ai.generationconfig.md#generationconfig_interface) | Config options for content-related requests | | [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface) | Interface for sending an image. | -| [GroundingAttribution](./ai.groundingattribution.md#groundingattribution_interface) | | -| [GroundingMetadata](./ai.groundingmetadata.md#groundingmetadata_interface) | Metadata returned to client when grounding is enabled. | +| [GoogleSearch](./ai.googlesearch.md#googlesearch_interface) | Specifies the Google Search configuration. | +| [GoogleSearchTool](./ai.googlesearchtool.md#googlesearchtool_interface) | A tool that allows a Gemini model to connect to Google Search to access and incorporate up-to-date information from the web into its responses.Important: If using Grounding with Google Search, you are required to comply with the "Grounding with Google Search" usage requirements for your chosen API provider: [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) section within the Service Specific Terms). | +| [GroundingChunk](./ai.groundingchunk.md#groundingchunk_interface) | Represents a chunk of retrieved data that supports a claim in the model's response. This is part of the grounding information provided when grounding is enabled. | +| [GroundingMetadata](./ai.groundingmetadata.md#groundingmetadata_interface) | Metadata returned when grounding is enabled.Currently, only Grounding with Google Search is supported (see [GoogleSearchTool](./ai.googlesearchtool.md#googlesearchtool_interface)).Important: If using Grounding with Google Search, you are required to comply with the "Grounding with Google Search" usage requirements for your chosen API provider: [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) section within the Service Specific Terms). | +| [GroundingSupport](./ai.groundingsupport.md#groundingsupport_interface) | Provides information about how a specific segment of the model's response is supported by the retrieved grounding chunks. | +| [HybridParams](./ai.hybridparams.md#hybridparams_interface) | (Public Preview) Configures hybrid inference. | | [ImagenGCSImage](./ai.imagengcsimage.md#imagengcsimage_interface) | An image generated by Imagen, stored in a Cloud Storage for Firebase bucket.This feature is not available yet. | -| [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface) | (Public Preview) Configuration options for generating images with Imagen.See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images-imagen) for more details. | -| [ImagenGenerationResponse](./ai.imagengenerationresponse.md#imagengenerationresponse_interface) | (Public Preview) The response from a request to generate images with Imagen. | -| [ImagenInlineImage](./ai.imageninlineimage.md#imageninlineimage_interface) | (Public Preview) An image generated by Imagen, represented as inline data. | -| [ImagenModelParams](./ai.imagenmodelparams.md#imagenmodelparams_interface) | (Public Preview) Parameters for configuring an [ImagenModel](./ai.imagenmodel.md#imagenmodel_class). | -| [ImagenSafetySettings](./ai.imagensafetysettings.md#imagensafetysettings_interface) | (Public Preview) Settings for controlling the aggressiveness of filtering out sensitive content.See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details. | +| [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface) | Configuration options for generating images with Imagen.See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images-imagen) for more details. | +| [ImagenGenerationResponse](./ai.imagengenerationresponse.md#imagengenerationresponse_interface) | The response from a request to generate images with Imagen. | +| [ImagenInlineImage](./ai.imageninlineimage.md#imageninlineimage_interface) | An image generated by Imagen, represented as inline data. | +| [ImagenModelParams](./ai.imagenmodelparams.md#imagenmodelparams_interface) | Parameters for configuring an [ImagenModel](./ai.imagenmodel.md#imagenmodel_class). | +| [ImagenSafetySettings](./ai.imagensafetysettings.md#imagensafetysettings_interface) | Settings for controlling the aggressiveness of filtering out sensitive content.See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details. | | [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface) | Content part interface if the part represents an image. | +| [LanguageModelCreateCoreOptions](./ai.languagemodelcreatecoreoptions.md#languagemodelcreatecoreoptions_interface) | (Public Preview) Configures the creation of an on-device language model session. | +| [LanguageModelCreateOptions](./ai.languagemodelcreateoptions.md#languagemodelcreateoptions_interface) | (Public Preview) Configures the creation of an on-device language model session. | +| [LanguageModelExpected](./ai.languagemodelexpected.md#languagemodelexpected_interface) | (Public Preview) Options for the expected inputs for an on-device language model. | +| [LanguageModelMessage](./ai.languagemodelmessage.md#languagemodelmessage_interface) | (Public Preview) An on-device language model message. | +| [LanguageModelMessageContent](./ai.languagemodelmessagecontent.md#languagemodelmessagecontent_interface) | (Public Preview) An on-device language model content object. | +| [LanguageModelPromptOptions](./ai.languagemodelpromptoptions.md#languagemodelpromptoptions_interface) | (Public Preview) Options for an on-device language model prompt. | +| [LiveGenerationConfig](./ai.livegenerationconfig.md#livegenerationconfig_interface) | (Public Preview) Configuration parameters used by [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) to control live content generation. | +| [LiveModelParams](./ai.livemodelparams.md#livemodelparams_interface) | (Public Preview) Params passed to [getLiveGenerativeModel()](./ai.md#getlivegenerativemodel_f2099ac). | +| [LiveServerContent](./ai.liveservercontent.md#liveservercontent_interface) | (Public Preview) An incremental content update from the model. | +| [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface) | (Public Preview) A request from the model for the client to execute one or more functions. | +| [LiveServerToolCallCancellation](./ai.liveservertoolcallcancellation.md#liveservertoolcallcancellation_interface) | (Public Preview) Notification to cancel a previous function call triggered by [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface). | | [ModalityTokenCount](./ai.modalitytokencount.md#modalitytokencount_interface) | Represents token counting info for a single modality. | -| [ModelParams](./ai.modelparams.md#modelparams_interface) | Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_80bd839). | -| [ObjectSchemaInterface](./ai.objectschemainterface.md#objectschemainterface_interface) | Interface for [ObjectSchema](./ai.objectschema.md#objectschema_class) class. | +| [ModelParams](./ai.modelparams.md#modelparams_interface) | Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_c63f46a). | +| [ObjectSchemaRequest](./ai.objectschemarequest.md#objectschemarequest_interface) | Interface for JSON parameters in a schema of [SchemaType](./ai.md#schematype) "object" when not using the Schema.object() helper. | +| [OnDeviceParams](./ai.ondeviceparams.md#ondeviceparams_interface) | (Public Preview) Encapsulates configuration for on-device inference. | +| [PrebuiltVoiceConfig](./ai.prebuiltvoiceconfig.md#prebuiltvoiceconfig_interface) | (Public Preview) Configuration for a pre-built voice. | | [PromptFeedback](./ai.promptfeedback.md#promptfeedback_interface) | If the prompt was blocked, this will be populated with blockReason and the relevant safetyRatings. | -| [RequestOptions](./ai.requestoptions.md#requestoptions_interface) | Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_80bd839). | +| [RequestOptions](./ai.requestoptions.md#requestoptions_interface) | Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_c63f46a). | | [RetrievedContextAttribution](./ai.retrievedcontextattribution.md#retrievedcontextattribution_interface) | | | [SafetyRating](./ai.safetyrating.md#safetyrating_interface) | A safety rating associated with a [GenerateContentCandidate](./ai.generatecontentcandidate.md#generatecontentcandidate_interface) | | [SafetySetting](./ai.safetysetting.md#safetysetting_interface) | Safety setting that can be sent as part of request parameters. | @@ -116,36 +126,83 @@ The Firebase AI Web SDK. | [SchemaParams](./ai.schemaparams.md#schemaparams_interface) | Params passed to [Schema](./ai.schema.md#schema_class) static methods to create specific [Schema](./ai.schema.md#schema_class) classes. | | [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) | Final format for [Schema](./ai.schema.md#schema_class) params passed to backend requests. | | [SchemaShared](./ai.schemashared.md#schemashared_interface) | Basic [Schema](./ai.schema.md#schema_class) properties shared across several Schema-related types. | -| [Segment](./ai.segment.md#segment_interface) | | +| [SearchEntrypoint](./ai.searchentrypoint.md#searchentrypoint_interface) | Google search entry point. | +| [Segment](./ai.segment.md#segment_interface) | Represents a specific segment within a [Content](./ai.content.md#content_interface) object, often used to pinpoint the exact location of text or data that grounding information refers to. | +| [SpeechConfig](./ai.speechconfig.md#speechconfig_interface) | (Public Preview) Configures speech synthesis. | +| [StartAudioConversationOptions](./ai.startaudioconversationoptions.md#startaudioconversationoptions_interface) | (Public Preview) Options for [startAudioConversation()](./ai.md#startaudioconversation_01c8e7f). | | [StartChatParams](./ai.startchatparams.md#startchatparams_interface) | Params for [GenerativeModel.startChat()](./ai.generativemodel.md#generativemodelstartchat). | | [TextPart](./ai.textpart.md#textpart_interface) | Content part interface if the part represents a text string. | +| [ThinkingConfig](./ai.thinkingconfig.md#thinkingconfig_interface) | Configuration for "thinking" behavior of compatible Gemini models.Certain models utilize a thinking process before generating a response. This allows them to reason through complex problems and plan a more coherent and accurate answer. | | [ToolConfig](./ai.toolconfig.md#toolconfig_interface) | Tool config. This config is shared for all tools provided in the request. | +| [URLContext](./ai.urlcontext.md#urlcontext_interface) | (Public Preview) Specifies the URL Context configuration. | +| [URLContextMetadata](./ai.urlcontextmetadata.md#urlcontextmetadata_interface) | (Public Preview) Metadata related to [URLContextTool](./ai.urlcontexttool.md#urlcontexttool_interface). | +| [URLContextTool](./ai.urlcontexttool.md#urlcontexttool_interface) | (Public Preview) A tool that allows you to provide additional context to the models in the form of public web URLs. By including URLs in your request, the Gemini model will access the content from those pages to inform and enhance its response. | +| [URLMetadata](./ai.urlmetadata.md#urlmetadata_interface) | (Public Preview) Metadata for a single URL retrieved by the [URLContextTool](./ai.urlcontexttool.md#urlcontexttool_interface) tool. | | [UsageMetadata](./ai.usagemetadata.md#usagemetadata_interface) | Usage metadata about a [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | -| [VertexAIOptions](./ai.vertexaioptions.md#vertexaioptions_interface) | Options when initializing the Firebase AI SDK. | | [VideoMetadata](./ai.videometadata.md#videometadata_interface) | Describes the input video content. | +| [VoiceConfig](./ai.voiceconfig.md#voiceconfig_interface) | (Public Preview) Configuration for the voice to used in speech synthesis. | | [WebAttribution](./ai.webattribution.md#webattribution_interface) | | +| [WebGroundingChunk](./ai.webgroundingchunk.md#webgroundingchunk_interface) | A grounding chunk from the web.Important: If using Grounding with Google Search, you are required to comply with the [Service Specific Terms](https://cloud.google.com/terms/service-terms) for "Grounding with Google Search". | ## Variables | Variable | Description | | --- | --- | +| [AIErrorCode](./ai.md#aierrorcode) | Standardized error codes that [AIError](./ai.aierror.md#aierror_class) can have. | | [BackendType](./ai.md#backendtype) | An enum-like object containing constants that represent the supported backends for the Firebase AI SDK. This determines which backend service (Vertex AI Gemini API or Gemini Developer API) the SDK will communicate with.These values are assigned to the backendType property within the specific backend configuration objects ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class) or [VertexAIBackend](./ai.vertexaibackend.md#vertexaibackend_class)) to identify which service to target. | +| [BlockReason](./ai.md#blockreason) | Reason that a prompt was blocked. | +| [FinishReason](./ai.md#finishreason) | Reason that a candidate finished. | +| [FunctionCallingMode](./ai.md#functioncallingmode) | | +| [HarmBlockMethod](./ai.md#harmblockmethod) | This property is not supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). | +| [HarmBlockThreshold](./ai.md#harmblockthreshold) | Threshold above which a prompt or candidate will be blocked. | +| [HarmCategory](./ai.md#harmcategory) | Harm categories that would cause prompts or candidates to be blocked. | +| [HarmProbability](./ai.md#harmprobability) | Probability that a prompt or candidate matches a harm category. | +| [HarmSeverity](./ai.md#harmseverity) | Harm severity levels. | +| [ImagenAspectRatio](./ai.md#imagenaspectratio) | Aspect ratios for Imagen images.To specify an aspect ratio for generated images, set the aspectRatio property in your [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface).See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. | +| [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel) | A filter level controlling whether generation of images containing people or faces is allowed.See the personGeneration documentation for more details. | +| [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel) | A filter level controlling how aggressively to filter sensitive content.Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, violence, sexual, derogatory, and toxic). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. | +| [InferenceMode](./ai.md#inferencemode) | (Public Preview) Determines whether inference happens on-device or in-cloud. | +| [Language](./ai.md#language) | (Public Preview) The programming language of the code. | +| [LiveResponseType](./ai.md#liveresponsetype) | (Public Preview) The types of responses that can be returned by [LiveSession.receive()](./ai.livesession.md#livesessionreceive). | +| [Modality](./ai.md#modality) | Content part modality. | +| [Outcome](./ai.md#outcome) | (Public Preview) Represents the result of the code execution. | | [POSSIBLE\_ROLES](./ai.md#possible_roles) | Possible roles. | | [ResponseModality](./ai.md#responsemodality) | (Public Preview) Generation modalities to be returned in generation responses. | -| [VertexAIError](./ai.md#vertexaierror) | | -| [VertexAIModel](./ai.md#vertexaimodel) | | +| [SchemaType](./ai.md#schematype) | Contains the list of OpenAPI data types as defined by the [OpenAPI specification](https://swagger.io/docs/specification/data-models/data-types/) | +| [URLRetrievalStatus](./ai.md#urlretrievalstatus) | (Public Preview) The status of a URL retrieval. | ## Type Aliases | Type Alias | Description | | --- | --- | +| [AIErrorCode](./ai.md#aierrorcode) | Standardized error codes that [AIError](./ai.aierror.md#aierror_class) can have. | | [BackendType](./ai.md#backendtype) | Type alias representing valid backend types. It can be either 'VERTEX_AI' or 'GOOGLE_AI'. | +| [BlockReason](./ai.md#blockreason) | Reason that a prompt was blocked. | +| [FinishReason](./ai.md#finishreason) | Reason that a candidate finished. | +| [FunctionCallingMode](./ai.md#functioncallingmode) | | +| [HarmBlockMethod](./ai.md#harmblockmethod) | This property is not supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). | +| [HarmBlockThreshold](./ai.md#harmblockthreshold) | Threshold above which a prompt or candidate will be blocked. | +| [HarmCategory](./ai.md#harmcategory) | Harm categories that would cause prompts or candidates to be blocked. | +| [HarmProbability](./ai.md#harmprobability) | Probability that a prompt or candidate matches a harm category. | +| [HarmSeverity](./ai.md#harmseverity) | Harm severity levels. | +| [ImagenAspectRatio](./ai.md#imagenaspectratio) | Aspect ratios for Imagen images.To specify an aspect ratio for generated images, set the aspectRatio property in your [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface).See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. | +| [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel) | A filter level controlling whether generation of images containing people or faces is allowed.See the personGeneration documentation for more details. | +| [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel) | A filter level controlling how aggressively to filter sensitive content.Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, violence, sexual, derogatory, and toxic). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. | +| [InferenceMode](./ai.md#inferencemode) | (Public Preview) Determines whether inference happens on-device or in-cloud. | +| [Language](./ai.md#language) | (Public Preview) The programming language of the code. | +| [LanguageModelMessageContentValue](./ai.md#languagemodelmessagecontentvalue) | (Public Preview) Content formats that can be provided as on-device message content. | +| [LanguageModelMessageRole](./ai.md#languagemodelmessagerole) | (Public Preview) Allowable roles for on-device language model usage. | +| [LanguageModelMessageType](./ai.md#languagemodelmessagetype) | (Public Preview) Allowable types for on-device language model messages. | +| [LiveResponseType](./ai.md#liveresponsetype) | (Public Preview) The types of responses that can be returned by [LiveSession.receive()](./ai.livesession.md#livesessionreceive). This is a property on all messages that can be used for type narrowing. This property is not returned by the server, it is assigned to a server message object once it's parsed. | +| [Modality](./ai.md#modality) | Content part modality. | +| [Outcome](./ai.md#outcome) | (Public Preview) Represents the result of the code execution. | | [Part](./ai.md#part) | Content part - includes text, image/video, or function call/response part types. | | [ResponseModality](./ai.md#responsemodality) | (Public Preview) Generation modalities to be returned in generation responses. | | [Role](./ai.md#role) | Role is the producer of the content. | +| [SchemaType](./ai.md#schematype) | Contains the list of OpenAPI data types as defined by the [OpenAPI specification](https://swagger.io/docs/specification/data-models/data-types/) | | [Tool](./ai.md#tool) | Defines a tool that model can call to access external knowledge. | | [TypedSchema](./ai.md#typedschema) | A type that includes all specific Schema types. | -| [VertexAI](./ai.md#vertexai) | | +| [URLRetrievalStatus](./ai.md#urlretrievalstatus) | (Public Preview) The status of a URL retrieval. | ## function(app, ...) @@ -198,69 +255,71 @@ const ai = getAI(app, { backend: new VertexAIBackend() }); ``` -### getVertexAI(app, options) {:#getvertexai_04094cf} +## function(ai, ...) -> Warning: This API is now obsolete. -> -> Use the new [getAI()](./ai.md#getai_a94a413) instead. The Vertex AI in Firebase SDK has been replaced with the Firebase AI SDK to accommodate the evolving set of supported features and services. For migration details, see the [migration guide](https://firebase.google.com/docs/vertex-ai/migrate-to-latest-sdk). -> -> Returns a [VertexAI](./ai.md#vertexai) instance for the given app, configured to use the Vertex AI Gemini API. This instance will be configured to use the Vertex AI Gemini API. -> +### getGenerativeModel(ai, modelParams, requestOptions) {:#getgenerativemodel_c63f46a} + +Returns a [GenerativeModel](./ai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. Signature: ```typescript -export declare function getVertexAI(app?: FirebaseApp, options?: VertexAIOptions): VertexAI; +export declare function getGenerativeModel(ai: AI, modelParams: ModelParams | HybridParams, requestOptions?: RequestOptions): GenerativeModel; ``` #### Parameters | Parameter | Type | Description | | --- | --- | --- | -| app | [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) | The [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) to use. | -| options | [VertexAIOptions](./ai.vertexaioptions.md#vertexaioptions_interface) | Options to configure the Vertex AI instance, including the location. | +| ai | [AI](./ai.ai.md#ai_interface) | | +| modelParams | [ModelParams](./ai.modelparams.md#modelparams_interface) \| [HybridParams](./ai.hybridparams.md#hybridparams_interface) | | +| requestOptions | [RequestOptions](./ai.requestoptions.md#requestoptions_interface) | | Returns: -[VertexAI](./ai.md#vertexai) +[GenerativeModel](./ai.generativemodel.md#generativemodel_class) -## function(ai, ...) +### getImagenModel(ai, modelParams, requestOptions) {:#getimagenmodel_e1f6645} -### getGenerativeModel(ai, modelParams, requestOptions) {:#getgenerativemodel_80bd839} +Returns an [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen. -Returns a [GenerativeModel](./ai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. +Only Imagen 3 models (named `imagen-3.0-*`) are supported. Signature: ```typescript -export declare function getGenerativeModel(ai: AI, modelParams: ModelParams, requestOptions?: RequestOptions): GenerativeModel; +export declare function getImagenModel(ai: AI, modelParams: ImagenModelParams, requestOptions?: RequestOptions): ImagenModel; ``` #### Parameters | Parameter | Type | Description | | --- | --- | --- | -| ai | [AI](./ai.ai.md#ai_interface) | | -| modelParams | [ModelParams](./ai.modelparams.md#modelparams_interface) | | -| requestOptions | [RequestOptions](./ai.requestoptions.md#requestoptions_interface) | | +| ai | [AI](./ai.ai.md#ai_interface) | An [AI](./ai.ai.md#ai_interface) instance. | +| modelParams | [ImagenModelParams](./ai.imagenmodelparams.md#imagenmodelparams_interface) | Parameters to use when making Imagen requests. | +| requestOptions | [RequestOptions](./ai.requestoptions.md#requestoptions_interface) | Additional options to use when making requests. | Returns: -[GenerativeModel](./ai.generativemodel.md#generativemodel_class) +[ImagenModel](./ai.imagenmodel.md#imagenmodel_class) -### getImagenModel(ai, modelParams, requestOptions) {:#getimagenmodel_e1f6645} +#### Exceptions + +If the `apiKey` or `projectId` fields are missing in your Firebase config. + +### getLiveGenerativeModel(ai, modelParams) {:#getlivegenerativemodel_f2099ac} > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -Returns an [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen. +Returns a [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) class for real-time, bidirectional communication. -Only Imagen 3 models (named `imagen-3.0-*`) are supported. +The Live API is only supported in modern browser windows and Node >= 22. Signature: ```typescript -export declare function getImagenModel(ai: AI, modelParams: ImagenModelParams, requestOptions?: RequestOptions): ImagenModel; +export declare function getLiveGenerativeModel(ai: AI, modelParams: LiveModelParams): LiveGenerativeModel; ``` #### Parameters @@ -268,17 +327,111 @@ export declare function getImagenModel(ai: AI, modelParams: ImagenModelParams, r | Parameter | Type | Description | | --- | --- | --- | | ai | [AI](./ai.ai.md#ai_interface) | An [AI](./ai.ai.md#ai_interface) instance. | -| modelParams | [ImagenModelParams](./ai.imagenmodelparams.md#imagenmodelparams_interface) | Parameters to use when making Imagen requests. | -| requestOptions | [RequestOptions](./ai.requestoptions.md#requestoptions_interface) | Additional options to use when making requests. | +| modelParams | [LiveModelParams](./ai.livemodelparams.md#livemodelparams_interface) | Parameters to use when setting up a [LiveSession](./ai.livesession.md#livesession_class). | Returns: -[ImagenModel](./ai.imagenmodel.md#imagenmodel_class) +[LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) #### Exceptions If the `apiKey` or `projectId` fields are missing in your Firebase config. +## function(liveSession, ...) + +### startAudioConversation(liveSession, options) {:#startaudioconversation_01c8e7f} + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Starts a real-time, bidirectional audio conversation with the model. This helper function manages the complexities of microphone access, audio recording, playback, and interruptions. + +Important: This function must be called in response to a user gesture (for example, a button click) to comply with [browser autoplay policies](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy). + +Signature: + +```typescript +export declare function startAudioConversation(liveSession: LiveSession, options?: StartAudioConversationOptions): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| liveSession | [LiveSession](./ai.livesession.md#livesession_class) | An active [LiveSession](./ai.livesession.md#livesession_class) instance. | +| options | [StartAudioConversationOptions](./ai.startaudioconversationoptions.md#startaudioconversationoptions_interface) | Configuration options for the audio conversation. | + +Returns: + +Promise<[AudioConversationController](./ai.audioconversationcontroller.md#audioconversationcontroller_interface)> + +A `Promise` that resolves with an [AudioConversationController](./ai.audioconversationcontroller.md#audioconversationcontroller_interface). + +#### Exceptions + +`AIError` if the environment does not support required Web APIs (`UNSUPPORTED`), if a conversation is already active (`REQUEST_ERROR`), the session is closed (`SESSION_CLOSED`), or if an unexpected initialization error occurs (`ERROR`). + +`DOMException` Thrown by `navigator.mediaDevices.getUserMedia()` if issues occur with microphone access, such as permissions being denied (`NotAllowedError`) or no compatible hardware being found (`NotFoundError`). See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#exceptions) for a full list of exceptions. + +### Example + + +```javascript +const liveSession = await model.connect(); +let conversationController; + +// This function must be called from within a click handler. +async function startConversation() { + try { + conversationController = await startAudioConversation(liveSession); + } catch (e) { + // Handle AI-specific errors + if (e instanceof AIError) { + console.error("AI Error:", e.message); + } + // Handle microphone permission and hardware errors + else if (e instanceof DOMException) { + console.error("Microphone Error:", e.message); + } + // Handle other unexpected errors + else { + console.error("An unexpected error occurred:", e); + } + } +} + +// Later, to stop the conversation: +// if (conversationController) { +// await conversationController.stop(); +// } + +``` + +## AIErrorCode + +Standardized error codes that [AIError](./ai.aierror.md#aierror_class) can have. + +Signature: + +```typescript +AIErrorCode: { + readonly ERROR: "error"; + readonly REQUEST_ERROR: "request-error"; + readonly RESPONSE_ERROR: "response-error"; + readonly FETCH_ERROR: "fetch-error"; + readonly SESSION_CLOSED: "session-closed"; + readonly INVALID_CONTENT: "invalid-content"; + readonly API_NOT_ENABLED: "api-not-enabled"; + readonly INVALID_SCHEMA: "invalid-schema"; + readonly NO_API_KEY: "no-api-key"; + readonly NO_APP_ID: "no-app-id"; + readonly NO_MODEL: "no-model"; + readonly NO_PROJECT_ID: "no-project-id"; + readonly PARSE_FAILED: "parse-failed"; + readonly UNSUPPORTED: "unsupported"; +} +``` + ## BackendType An enum-like object containing constants that represent the supported backends for the Firebase AI SDK. This determines which backend service (Vertex AI Gemini API or Gemini Developer API) the SDK will communicate with. @@ -294,138 +447,333 @@ BackendType: { } ``` -## POSSIBLE\_ROLES +## BlockReason -Possible roles. +Reason that a prompt was blocked. Signature: ```typescript -POSSIBLE_ROLES: readonly ["user", "model", "function", "system"] +BlockReason: { + readonly SAFETY: "SAFETY"; + readonly OTHER: "OTHER"; + readonly BLOCKLIST: "BLOCKLIST"; + readonly PROHIBITED_CONTENT: "PROHIBITED_CONTENT"; +} ``` -## ResponseModality +## FinishReason -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> +Reason that a candidate finished. + +Signature: + +```typescript +FinishReason: { + readonly STOP: "STOP"; + readonly MAX_TOKENS: "MAX_TOKENS"; + readonly SAFETY: "SAFETY"; + readonly RECITATION: "RECITATION"; + readonly OTHER: "OTHER"; + readonly BLOCKLIST: "BLOCKLIST"; + readonly PROHIBITED_CONTENT: "PROHIBITED_CONTENT"; + readonly SPII: "SPII"; + readonly MALFORMED_FUNCTION_CALL: "MALFORMED_FUNCTION_CALL"; +} +``` + +## FunctionCallingMode -Generation modalities to be returned in generation responses. Signature: ```typescript -ResponseModality: { - readonly TEXT: "TEXT"; - readonly IMAGE: "IMAGE"; +FunctionCallingMode: { + readonly AUTO: "AUTO"; + readonly ANY: "ANY"; + readonly NONE: "NONE"; } ``` -## VertexAIError +## HarmBlockMethod -> Warning: This API is now obsolete. -> -> Use the new [AIError](./ai.aierror.md#aierror_class) instead. The Vertex AI in Firebase SDK has been replaced with the Firebase AI SDK to accommodate the evolving set of supported features and services. For migration details, see the [migration guide](https://firebase.google.com/docs/vertex-ai/migrate-to-latest-sdk). -> -> Error class for the Firebase AI SDK. -> +This property is not supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). Signature: ```typescript -VertexAIError: typeof AIError +HarmBlockMethod: { + readonly SEVERITY: "SEVERITY"; + readonly PROBABILITY: "PROBABILITY"; +} ``` -## VertexAIModel +## HarmBlockThreshold -> Warning: This API is now obsolete. -> -> Use the new [AIModel](./ai.aimodel.md#aimodel_class) instead. The Vertex AI in Firebase SDK has been replaced with the Firebase AI SDK to accommodate the evolving set of supported features and services. For migration details, see the [migration guide](https://firebase.google.com/docs/vertex-ai/migrate-to-latest-sdk). -> -> Base class for Firebase AI model APIs. -> +Threshold above which a prompt or candidate will be blocked. Signature: ```typescript -VertexAIModel: typeof AIModel +HarmBlockThreshold: { + readonly BLOCK_LOW_AND_ABOVE: "BLOCK_LOW_AND_ABOVE"; + readonly BLOCK_MEDIUM_AND_ABOVE: "BLOCK_MEDIUM_AND_ABOVE"; + readonly BLOCK_ONLY_HIGH: "BLOCK_ONLY_HIGH"; + readonly BLOCK_NONE: "BLOCK_NONE"; + readonly OFF: "OFF"; +} ``` -## BackendType +## HarmCategory -Type alias representing valid backend types. It can be either `'VERTEX_AI'` or `'GOOGLE_AI'`. +Harm categories that would cause prompts or candidates to be blocked. Signature: ```typescript -export type BackendType = (typeof BackendType)[keyof typeof BackendType]; +HarmCategory: { + readonly HARM_CATEGORY_HATE_SPEECH: "HARM_CATEGORY_HATE_SPEECH"; + readonly HARM_CATEGORY_SEXUALLY_EXPLICIT: "HARM_CATEGORY_SEXUALLY_EXPLICIT"; + readonly HARM_CATEGORY_HARASSMENT: "HARM_CATEGORY_HARASSMENT"; + readonly HARM_CATEGORY_DANGEROUS_CONTENT: "HARM_CATEGORY_DANGEROUS_CONTENT"; +} ``` -## Part +## HarmProbability -Content part - includes text, image/video, or function call/response part types. +Probability that a prompt or candidate matches a harm category. Signature: ```typescript -export type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | FileDataPart; +HarmProbability: { + readonly NEGLIGIBLE: "NEGLIGIBLE"; + readonly LOW: "LOW"; + readonly MEDIUM: "MEDIUM"; + readonly HIGH: "HIGH"; +} ``` -## ResponseModality +## HarmSeverity + +Harm severity levels. + +Signature: + +```typescript +HarmSeverity: { + readonly HARM_SEVERITY_NEGLIGIBLE: "HARM_SEVERITY_NEGLIGIBLE"; + readonly HARM_SEVERITY_LOW: "HARM_SEVERITY_LOW"; + readonly HARM_SEVERITY_MEDIUM: "HARM_SEVERITY_MEDIUM"; + readonly HARM_SEVERITY_HIGH: "HARM_SEVERITY_HIGH"; + readonly HARM_SEVERITY_UNSUPPORTED: "HARM_SEVERITY_UNSUPPORTED"; +} +``` + +## ImagenAspectRatio + +Aspect ratios for Imagen images. + +To specify an aspect ratio for generated images, set the `aspectRatio` property in your [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface). + +See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. + +Signature: + +```typescript +ImagenAspectRatio: { + readonly SQUARE: "1:1"; + readonly LANDSCAPE_3x4: "3:4"; + readonly PORTRAIT_4x3: "4:3"; + readonly LANDSCAPE_16x9: "16:9"; + readonly PORTRAIT_9x16: "9:16"; +} +``` + +## ImagenPersonFilterLevel + +A filter level controlling whether generation of images containing people or faces is allowed. + +See the personGeneration documentation for more details. + +Signature: + +```typescript +ImagenPersonFilterLevel: { + readonly BLOCK_ALL: "dont_allow"; + readonly ALLOW_ADULT: "allow_adult"; + readonly ALLOW_ALL: "allow_all"; +} +``` + +## ImagenSafetyFilterLevel + +A filter level controlling how aggressively to filter sensitive content. + +Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, `violence`, `sexual`, `derogatory`, and `toxic`). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. + +Signature: + +```typescript +ImagenSafetyFilterLevel: { + readonly BLOCK_LOW_AND_ABOVE: "block_low_and_above"; + readonly BLOCK_MEDIUM_AND_ABOVE: "block_medium_and_above"; + readonly BLOCK_ONLY_HIGH: "block_only_high"; + readonly BLOCK_NONE: "block_none"; +} +``` + +## InferenceMode > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -Generation modalities to be returned in generation responses. +Determines whether inference happens on-device or in-cloud. + +PREFER\_ON\_DEVICE: Attempt to make inference calls using an on-device model. If on-device inference is not available, the SDK will fall back to using a cloud-hosted model.
ONLY\_ON\_DEVICE: Only attempt to make inference calls using an on-device model. The SDK will not fall back to a cloud-hosted model. If on-device inference is not available, inference methods will throw.
ONLY\_IN\_CLOUD: Only attempt to make inference calls using a cloud-hosted model. The SDK will not fall back to an on-device model.
PREFER\_IN\_CLOUD: Attempt to make inference calls to a cloud-hosted model. If not available, the SDK will fall back to an on-device model. Signature: ```typescript -export type ResponseModality = (typeof ResponseModality)[keyof typeof ResponseModality]; +InferenceMode: { + readonly PREFER_ON_DEVICE: "prefer_on_device"; + readonly ONLY_ON_DEVICE: "only_on_device"; + readonly ONLY_IN_CLOUD: "only_in_cloud"; + readonly PREFER_IN_CLOUD: "prefer_in_cloud"; +} ``` -## Role +## Language -Role is the producer of the content. +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The programming language of the code. Signature: ```typescript -export type Role = (typeof POSSIBLE_ROLES)[number]; +Language: { + UNSPECIFIED: string; + PYTHON: string; +} ``` -## Tool +## LiveResponseType -Defines a tool that model can call to access external knowledge. +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The types of responses that can be returned by [LiveSession.receive()](./ai.livesession.md#livesessionreceive). Signature: ```typescript -export declare type Tool = FunctionDeclarationsTool; +LiveResponseType: { + SERVER_CONTENT: string; + TOOL_CALL: string; + TOOL_CALL_CANCELLATION: string; +} ``` -## TypedSchema +## Modality -A type that includes all specific Schema types. +Content part modality. Signature: ```typescript -export type TypedSchema = IntegerSchema | NumberSchema | StringSchema | BooleanSchema | ObjectSchema | ArraySchema; +Modality: { + readonly MODALITY_UNSPECIFIED: "MODALITY_UNSPECIFIED"; + readonly TEXT: "TEXT"; + readonly IMAGE: "IMAGE"; + readonly VIDEO: "VIDEO"; + readonly AUDIO: "AUDIO"; + readonly DOCUMENT: "DOCUMENT"; +} ``` -## VertexAI +## Outcome -> Warning: This API is now obsolete. +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -> Use the new [AI](./ai.ai.md#ai_interface) instead. The Vertex AI in Firebase SDK has been replaced with the Firebase AI SDK to accommodate the evolving set of supported features and services. For migration details, see the [migration guide](https://firebase.google.com/docs/vertex-ai/migrate-to-latest-sdk). + +Represents the result of the code execution. + +Signature: + +```typescript +Outcome: { + UNSPECIFIED: string; + OK: string; + FAILED: string; + DEADLINE_EXCEEDED: string; +} +``` + +## POSSIBLE\_ROLES + +Possible roles. + +Signature: + +```typescript +POSSIBLE_ROLES: readonly ["user", "model", "function", "system"] +``` + +## ResponseModality + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -> An instance of the Firebase AI SDK. + +Generation modalities to be returned in generation responses. + +Signature: + +```typescript +ResponseModality: { + readonly TEXT: "TEXT"; + readonly IMAGE: "IMAGE"; + readonly AUDIO: "AUDIO"; +} +``` + +## SchemaType + +Contains the list of OpenAPI data types as defined by the [OpenAPI specification](https://swagger.io/docs/specification/data-models/data-types/) + +Signature: + +```typescript +SchemaType: { + readonly STRING: "string"; + readonly NUMBER: "number"; + readonly INTEGER: "integer"; + readonly BOOLEAN: "boolean"; + readonly ARRAY: "array"; + readonly OBJECT: "object"; +} +``` + +## URLRetrievalStatus + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > +The status of a URL retrieval. + +URL\_RETRIEVAL\_STATUS\_UNSPECIFIED: Unspecified retrieval status.
URL\_RETRIEVAL\_STATUS\_SUCCESS: The URL retrieval was successful.
URL\_RETRIEVAL\_STATUS\_ERROR: The URL retrieval failed.
URL\_RETRIEVAL\_STATUS\_PAYWALL: The URL retrieval failed because the content is behind a paywall.
URL\_RETRIEVAL\_STATUS\_UNSAFE: The URL retrieval failed because the content is unsafe.
+ Signature: ```typescript -export type VertexAI = AI; +URLRetrievalStatus: { + URL_RETRIEVAL_STATUS_UNSPECIFIED: string; + URL_RETRIEVAL_STATUS_SUCCESS: string; + URL_RETRIEVAL_STATUS_ERROR: string; + URL_RETRIEVAL_STATUS_PAYWALL: string; + URL_RETRIEVAL_STATUS_UNSAFE: string; +} ``` ## AIErrorCode @@ -435,26 +783,18 @@ Standardized error codes that [AIError](./ai.aierror.md#aierror_class) can have. Signature: ```typescript -export declare const enum AIErrorCode +export type AIErrorCode = (typeof AIErrorCode)[keyof typeof AIErrorCode]; ``` -## Enumeration Members +## BackendType -| Member | Value | Description | -| --- | --- | --- | -| API\_NOT\_ENABLED | "api-not-enabled" | An error due to the Firebase API not being enabled in the Console. | -| ERROR | "error" | A generic error occurred. | -| FETCH\_ERROR | "fetch-error" | An error occurred while performing a fetch. | -| INVALID\_CONTENT | "invalid-content" | An error associated with a Content object. | -| INVALID\_SCHEMA | "invalid-schema" | An error due to invalid Schema input. | -| NO\_API\_KEY | "no-api-key" | An error occurred due to a missing Firebase API key. | -| NO\_APP\_ID | "no-app-id" | An error occurred due to a missing Firebase app ID. | -| NO\_MODEL | "no-model" | An error occurred due to a model name not being specified during initialization. | -| NO\_PROJECT\_ID | "no-project-id" | An error occurred due to a missing project ID. | -| PARSE\_FAILED | "parse-failed" | An error occurred while parsing. | -| REQUEST\_ERROR | "request-error" | An error occurred in a request. | -| RESPONSE\_ERROR | "response-error" | An error occurred in a response. | -| UNSUPPORTED | "unsupported" | An error occurred due an attempt to use an unsupported feature. | +Type alias representing valid backend types. It can be either `'VERTEX_AI'` or `'GOOGLE_AI'`. + +Signature: + +```typescript +export type BackendType = (typeof BackendType)[keyof typeof BackendType]; +``` ## BlockReason @@ -463,18 +803,9 @@ Reason that a prompt was blocked. Signature: ```typescript -export declare enum BlockReason +export type BlockReason = (typeof BlockReason)[keyof typeof BlockReason]; ``` -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| BLOCKLIST | "BLOCKLIST" | Content was blocked because it contained terms from the terminology blocklist. | -| OTHER | "OTHER" | Content was blocked, but the reason is uncategorized. | -| PROHIBITED\_CONTENT | "PROHIBITED_CONTENT" | Content was blocked due to prohibited content. | -| SAFETY | "SAFETY" | Content was blocked by safety settings. | - ## FinishReason Reason that a candidate finished. @@ -482,40 +813,18 @@ Reason that a candidate finished. Signature: ```typescript -export declare enum FinishReason +export type FinishReason = (typeof FinishReason)[keyof typeof FinishReason]; ``` -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| BLOCKLIST | "BLOCKLIST" | The candidate content contained forbidden terms. | -| MALFORMED\_FUNCTION\_CALL | "MALFORMED_FUNCTION_CALL" | The function call generated by the model was invalid. | -| MAX\_TOKENS | "MAX_TOKENS" | The maximum number of tokens as specified in the request was reached. | -| OTHER | "OTHER" | Unknown reason. | -| PROHIBITED\_CONTENT | "PROHIBITED_CONTENT" | The candidate content potentially contained prohibited content. | -| RECITATION | "RECITATION" | The candidate content was flagged for recitation reasons. | -| SAFETY | "SAFETY" | The candidate content was flagged for safety reasons. | -| SPII | "SPII" | The candidate content potentially contained Sensitive Personally Identifiable Information (SPII). | -| STOP | "STOP" | Natural stop point of the model or provided stop sequence. | - ## FunctionCallingMode Signature: ```typescript -export declare enum FunctionCallingMode +export type FunctionCallingMode = (typeof FunctionCallingMode)[keyof typeof FunctionCallingMode]; ``` -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| ANY | "ANY" | Model is constrained to always predicting a function call only. If allowed_function_names is set, the predicted function call will be limited to any one of allowed_function_names, else the predicted function call will be any one of the provided function_declarations. | -| AUTO | "AUTO" | Default model behavior; model decides to predict either a function call or a natural language response. | -| NONE | "NONE" | Model will not predict any function call. Model behavior is same as when not passing any function declarations. | - ## HarmBlockMethod This property is not supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). @@ -523,16 +832,9 @@ This property is not supported in the Gemini Developer API ([GoogleAIBackend](./ Signature: ```typescript -export declare enum HarmBlockMethod +export type HarmBlockMethod = (typeof HarmBlockMethod)[keyof typeof HarmBlockMethod]; ``` -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| PROBABILITY | "PROBABILITY" | The harm block method uses the probability score. | -| SEVERITY | "SEVERITY" | The harm block method uses both probability and severity scores. | - ## HarmBlockThreshold Threshold above which a prompt or candidate will be blocked. @@ -540,19 +842,9 @@ Threshold above which a prompt or candidate will be blocked. Signature: ```typescript -export declare enum HarmBlockThreshold +export type HarmBlockThreshold = (typeof HarmBlockThreshold)[keyof typeof HarmBlockThreshold]; ``` -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| BLOCK\_LOW\_AND\_ABOVE | "BLOCK_LOW_AND_ABOVE" | Content with NEGLIGIBLE will be allowed. | -| BLOCK\_MEDIUM\_AND\_ABOVE | "BLOCK_MEDIUM_AND_ABOVE" | Content with NEGLIGIBLE and LOW will be allowed. | -| BLOCK\_NONE | "BLOCK_NONE" | All content will be allowed. | -| BLOCK\_ONLY\_HIGH | "BLOCK_ONLY_HIGH" | Content with NEGLIGIBLE, LOW, and MEDIUM will be allowed. | -| OFF | "OFF" | All content will be allowed. This is the same as BLOCK_NONE, but the metadata corresponding to the [HarmCategory](./ai.md#harmcategory) will not be present in the response. | - ## HarmCategory Harm categories that would cause prompts or candidates to be blocked. @@ -560,18 +852,9 @@ Harm categories that would cause prompts or candidates to be blocked. Signature: ```typescript -export declare enum HarmCategory +export type HarmCategory = (typeof HarmCategory)[keyof typeof HarmCategory]; ``` -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| HARM\_CATEGORY\_DANGEROUS\_CONTENT | "HARM_CATEGORY_DANGEROUS_CONTENT" | | -| HARM\_CATEGORY\_HARASSMENT | "HARM_CATEGORY_HARASSMENT" | | -| HARM\_CATEGORY\_HATE\_SPEECH | "HARM_CATEGORY_HATE_SPEECH" | | -| HARM\_CATEGORY\_SEXUALLY\_EXPLICIT | "HARM_CATEGORY_SEXUALLY_EXPLICIT" | | - ## HarmProbability Probability that a prompt or candidate matches a harm category. @@ -579,18 +862,9 @@ Probability that a prompt or candidate matches a harm category. Signature: ```typescript -export declare enum HarmProbability +export type HarmProbability = (typeof HarmProbability)[keyof typeof HarmProbability]; ``` -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| HIGH | "HIGH" | Content has a high chance of being unsafe. | -| LOW | "LOW" | Content has a low chance of being unsafe. | -| MEDIUM | "MEDIUM" | Content has a medium chance of being unsafe. | -| NEGLIGIBLE | "NEGLIGIBLE" | Content has a negligible chance of being unsafe. | - ## HarmSeverity Harm severity levels. @@ -598,92 +872,124 @@ Harm severity levels. Signature: ```typescript -export declare enum HarmSeverity +export type HarmSeverity = (typeof HarmSeverity)[keyof typeof HarmSeverity]; ``` -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| HARM\_SEVERITY\_HIGH | "HARM_SEVERITY_HIGH" | High level of harm severity. | -| HARM\_SEVERITY\_LOW | "HARM_SEVERITY_LOW" | Low level of harm severity. | -| HARM\_SEVERITY\_MEDIUM | "HARM_SEVERITY_MEDIUM" | Medium level of harm severity. | -| HARM\_SEVERITY\_NEGLIGIBLE | "HARM_SEVERITY_NEGLIGIBLE" | Negligible level of harm severity. | -| HARM\_SEVERITY\_UNSUPPORTED | "HARM_SEVERITY_UNSUPPORTED" | Harm severity is not supported. | - ## ImagenAspectRatio -> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. -> - Aspect ratios for Imagen images. To specify an aspect ratio for generated images, set the `aspectRatio` property in your [ImagenGenerationConfig](./ai.imagengenerationconfig.md#imagengenerationconfig_interface). -See the the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. +See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. Signature: ```typescript -export declare enum ImagenAspectRatio +export type ImagenAspectRatio = (typeof ImagenAspectRatio)[keyof typeof ImagenAspectRatio]; ``` -## Enumeration Members +## ImagenPersonFilterLevel -| Member | Value | Description | -| --- | --- | --- | -| LANDSCAPE\_16x9 | "16:9" | (Public Preview) Landscape (16:9) aspect ratio. | -| LANDSCAPE\_3x4 | "3:4" | (Public Preview) Landscape (3:4) aspect ratio. | -| PORTRAIT\_4x3 | "4:3" | (Public Preview) Portrait (4:3) aspect ratio. | -| PORTRAIT\_9x16 | "9:16" | (Public Preview) Portrait (9:16) aspect ratio. | -| SQUARE | "1:1" | (Public Preview) Square (1:1) aspect ratio. | +A filter level controlling whether generation of images containing people or faces is allowed. -## ImagenPersonFilterLevel +See the personGeneration documentation for more details. + +Signature: + +```typescript +export type ImagenPersonFilterLevel = (typeof ImagenPersonFilterLevel)[keyof typeof ImagenPersonFilterLevel]; +``` + +## ImagenSafetyFilterLevel + +A filter level controlling how aggressively to filter sensitive content. + +Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, `violence`, `sexual`, `derogatory`, and `toxic`). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. + +Signature: + +```typescript +export type ImagenSafetyFilterLevel = (typeof ImagenSafetyFilterLevel)[keyof typeof ImagenSafetyFilterLevel]; +``` + +## InferenceMode > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -A filter level controlling whether generation of images containing people or faces is allowed. +Determines whether inference happens on-device or in-cloud. -See the personGeneration documentation for more details. +Signature: + +```typescript +export type InferenceMode = (typeof InferenceMode)[keyof typeof InferenceMode]; +``` + +## Language + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The programming language of the code. Signature: ```typescript -export declare enum ImagenPersonFilterLevel +export type Language = (typeof Language)[keyof typeof Language]; ``` -## Enumeration Members +## LanguageModelMessageContentValue -| Member | Value | Description | -| --- | --- | --- | -| ALLOW\_ADULT | "allow_adult" | (Public Preview) Allow generation of images containing adults only; images of children are filtered out.Generation of images containing people or faces may require your use case to be reviewed and approved by Cloud support; see the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) for more details. | -| ALLOW\_ALL | "allow_all" | (Public Preview) Allow generation of images containing adults only; images of children are filtered out.Generation of images containing people or faces may require your use case to be reviewed and approved by Cloud support; see the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) for more details. | -| BLOCK\_ALL | "dont_allow" | (Public Preview) Disallow generation of images containing people or faces; images of people are filtered out. | +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> -## ImagenSafetyFilterLevel +Content formats that can be provided as on-device message content. + +Signature: + +```typescript +export type LanguageModelMessageContentValue = ImageBitmapSource | AudioBuffer | BufferSource | string; +``` + +## LanguageModelMessageRole > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -A filter level controlling how aggressively to filter sensitive content. +Allowable roles for on-device language model usage. -Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, `violence`, `sexual`, `derogatory`, and `toxic`). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. +Signature: + +```typescript +export type LanguageModelMessageRole = 'system' | 'user' | 'assistant'; +``` + +## LanguageModelMessageType + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Allowable types for on-device language model messages. Signature: ```typescript -export declare enum ImagenSafetyFilterLevel +export type LanguageModelMessageType = 'text' | 'image' | 'audio'; ``` -## Enumeration Members +## LiveResponseType -| Member | Value | Description | -| --- | --- | --- | -| BLOCK\_LOW\_AND\_ABOVE | "block_low_and_above" | (Public Preview) The most aggressive filtering level; most strict blocking. | -| BLOCK\_MEDIUM\_AND\_ABOVE | "block_medium_and_above" | (Public Preview) Blocks some sensitive prompts and responses. | -| BLOCK\_NONE | "block_none" | (Public Preview) The least aggressive filtering level; blocks very few sensitive prompts and responses.Access to this feature is restricted and may require your case to be reviewed and approved by Cloud support. | -| BLOCK\_ONLY\_HIGH | "block_only_high" | (Public Preview) Blocks few sensitive prompts and responses. | +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The types of responses that can be returned by [LiveSession.receive()](./ai.livesession.md#livesessionreceive). This is a property on all messages that can be used for type narrowing. This property is not returned by the server, it is assigned to a server message object once it's parsed. + +Signature: + +```typescript +export type LiveResponseType = (typeof LiveResponseType)[keyof typeof LiveResponseType]; +``` ## Modality @@ -692,19 +998,54 @@ Content part modality. Signature: ```typescript -export declare enum Modality +export type Modality = (typeof Modality)[keyof typeof Modality]; ``` -## Enumeration Members +## Outcome -| Member | Value | Description | -| --- | --- | --- | -| AUDIO | "AUDIO" | Audio. | -| DOCUMENT | "DOCUMENT" | Document (for example, PDF). | -| IMAGE | "IMAGE" | Image. | -| MODALITY\_UNSPECIFIED | "MODALITY_UNSPECIFIED" | Unspecified modality. | -| TEXT | "TEXT" | Plain text. | -| VIDEO | "VIDEO" | Video. | +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Represents the result of the code execution. + +Signature: + +```typescript +export type Outcome = (typeof Outcome)[keyof typeof Outcome]; +``` + +## Part + +Content part - includes text, image/video, or function call/response part types. + +Signature: + +```typescript +export type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | FileDataPart | ExecutableCodePart | CodeExecutionResultPart; +``` + +## ResponseModality + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Generation modalities to be returned in generation responses. + +Signature: + +```typescript +export type ResponseModality = (typeof ResponseModality)[keyof typeof ResponseModality]; +``` + +## Role + +Role is the producer of the content. + +Signature: + +```typescript +export type Role = (typeof POSSIBLE_ROLES)[number]; +``` ## SchemaType @@ -713,17 +1054,40 @@ Contains the list of OpenAPI data types as defined by the [OpenAPI specification Signature: ```typescript -export declare enum SchemaType +export type SchemaType = (typeof SchemaType)[keyof typeof SchemaType]; ``` -## Enumeration Members +## Tool -| Member | Value | Description | -| --- | --- | --- | -| ARRAY | "array" | Array type. | -| BOOLEAN | "boolean" | Boolean type. | -| INTEGER | "integer" | Integer type. | -| NUMBER | "number" | Number type. | -| OBJECT | "object" | Object type. | -| STRING | "string" | String type. | +Defines a tool that model can call to access external knowledge. + +Signature: + +```typescript +export type Tool = FunctionDeclarationsTool | GoogleSearchTool | CodeExecutionTool | URLContextTool; +``` + +## TypedSchema + +A type that includes all specific Schema types. + +Signature: +```typescript +export type TypedSchema = IntegerSchema | NumberSchema | StringSchema | BooleanSchema | ObjectSchema | ArraySchema | AnyOfSchema; +``` + +## URLRetrievalStatus + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The status of a URL retrieval. + +URL\_RETRIEVAL\_STATUS\_UNSPECIFIED: Unspecified retrieval status.
URL\_RETRIEVAL\_STATUS\_SUCCESS: The URL retrieval was successful.
URL\_RETRIEVAL\_STATUS\_ERROR: The URL retrieval failed.
URL\_RETRIEVAL\_STATUS\_PAYWALL: The URL retrieval failed because the content is behind a paywall.
URL\_RETRIEVAL\_STATUS\_UNSAFE: The URL retrieval failed because the content is unsafe.
+ +Signature: + +```typescript +export type URLRetrievalStatus = (typeof URLRetrievalStatus)[keyof typeof URLRetrievalStatus]; +``` diff --git a/docs-devsite/ai.modelparams.md b/docs-devsite/ai.modelparams.md index a92b2e9035d..a5722e7d69d 100644 --- a/docs-devsite/ai.modelparams.md +++ b/docs-devsite/ai.modelparams.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ModelParams interface -Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_80bd839). +Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_c63f46a). Signature: diff --git a/docs-devsite/ai.objectschemainterface.md b/docs-devsite/ai.objectschemainterface.md deleted file mode 100644 index 15b1a97f40d..00000000000 --- a/docs-devsite/ai.objectschemainterface.md +++ /dev/null @@ -1,43 +0,0 @@ -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 %} - -# ObjectSchemaInterface interface -Interface for [ObjectSchema](./ai.objectschema.md#objectschema_class) class. - -Signature: - -```typescript -export interface ObjectSchemaInterface extends SchemaInterface -``` -Extends: [SchemaInterface](./ai.schemainterface.md#schemainterface_interface) - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [optionalProperties](./ai.objectschemainterface.md#objectschemainterfaceoptionalproperties) | string\[\] | | -| [type](./ai.objectschemainterface.md#objectschemainterfacetype) | [SchemaType.OBJECT](./ai.md#schematypeobject_enummember) | | - -## ObjectSchemaInterface.optionalProperties - -Signature: - -```typescript -optionalProperties?: string[]; -``` - -## ObjectSchemaInterface.type - -Signature: - -```typescript -type: SchemaType.OBJECT; -``` diff --git a/docs-devsite/ai.objectschemarequest.md b/docs-devsite/ai.objectschemarequest.md new file mode 100644 index 00000000000..267e2d43345 --- /dev/null +++ b/docs-devsite/ai.objectschemarequest.md @@ -0,0 +1,45 @@ +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 %} + +# ObjectSchemaRequest interface +Interface for JSON parameters in a schema of [SchemaType](./ai.md#schematype) "object" when not using the `Schema.object()` helper. + +Signature: + +```typescript +export interface ObjectSchemaRequest extends SchemaRequest +``` +Extends: [SchemaRequest](./ai.schemarequest.md#schemarequest_interface) + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [optionalProperties](./ai.objectschemarequest.md#objectschemarequestoptionalproperties) | never | This is not a property accepted in the final request to the backend, but is a client-side convenience property that is only usable by constructing a schema through the Schema.object() helper method. Populating this property will cause response errors if the object is not wrapped with Schema.object(). | +| [type](./ai.objectschemarequest.md#objectschemarequesttype) | 'object' | | + +## ObjectSchemaRequest.optionalProperties + +This is not a property accepted in the final request to the backend, but is a client-side convenience property that is only usable by constructing a schema through the `Schema.object()` helper method. Populating this property will cause response errors if the object is not wrapped with `Schema.object()`. + +Signature: + +```typescript +optionalProperties?: never; +``` + +## ObjectSchemaRequest.type + +Signature: + +```typescript +type: 'object'; +``` diff --git a/docs-devsite/ai.ondeviceparams.md b/docs-devsite/ai.ondeviceparams.md new file mode 100644 index 00000000000..363427149f9 --- /dev/null +++ b/docs-devsite/ai.ondeviceparams.md @@ -0,0 +1,51 @@ +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 %} + +# OnDeviceParams interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Encapsulates configuration for on-device inference. + +Signature: + +```typescript +export interface OnDeviceParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [createOptions](./ai.ondeviceparams.md#ondeviceparamscreateoptions) | [LanguageModelCreateOptions](./ai.languagemodelcreateoptions.md#languagemodelcreateoptions_interface) | (Public Preview) | +| [promptOptions](./ai.ondeviceparams.md#ondeviceparamspromptoptions) | [LanguageModelPromptOptions](./ai.languagemodelpromptoptions.md#languagemodelpromptoptions_interface) | (Public Preview) | + +## OnDeviceParams.createOptions + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +createOptions?: LanguageModelCreateOptions; +``` + +## OnDeviceParams.promptOptions + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +promptOptions?: LanguageModelPromptOptions; +``` diff --git a/docs-devsite/ai.prebuiltvoiceconfig.md b/docs-devsite/ai.prebuiltvoiceconfig.md new file mode 100644 index 00000000000..8627ae184b3 --- /dev/null +++ b/docs-devsite/ai.prebuiltvoiceconfig.md @@ -0,0 +1,43 @@ +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 %} + +# PrebuiltVoiceConfig interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configuration for a pre-built voice. + +Signature: + +```typescript +export interface PrebuiltVoiceConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [voiceName](./ai.prebuiltvoiceconfig.md#prebuiltvoiceconfigvoicename) | string | (Public Preview) The voice name to use for speech synthesis.For a full list of names and demos of what each voice sounds like, see [Chirp 3: HD Voices](https://cloud.google.com/text-to-speech/docs/chirp3-hd). | + +## PrebuiltVoiceConfig.voiceName + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The voice name to use for speech synthesis. + +For a full list of names and demos of what each voice sounds like, see [Chirp 3: HD Voices](https://cloud.google.com/text-to-speech/docs/chirp3-hd). + +Signature: + +```typescript +voiceName?: string; +``` diff --git a/docs-devsite/ai.requestoptions.md b/docs-devsite/ai.requestoptions.md index 73aa03c1d25..c04230fcd62 100644 --- a/docs-devsite/ai.requestoptions.md +++ b/docs-devsite/ai.requestoptions.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # RequestOptions interface -Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_80bd839). +Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_c63f46a). Signature: @@ -22,12 +22,12 @@ export interface RequestOptions | Property | Type | Description | | --- | --- | --- | -| [baseUrl](./ai.requestoptions.md#requestoptionsbaseurl) | string | Base url for endpoint. Defaults to https://firebasevertexai.googleapis.com | +| [baseUrl](./ai.requestoptions.md#requestoptionsbaseurl) | string | Base url for endpoint. Defaults to https://firebasevertexai.googleapis.com, which is the [Firebase AI Logic API](https://console.cloud.google.com/apis/library/firebasevertexai.googleapis.com?project=_) (used regardless of your chosen Gemini API provider). | | [timeout](./ai.requestoptions.md#requestoptionstimeout) | number | Request timeout in milliseconds. Defaults to 180 seconds (180000ms). | ## RequestOptions.baseUrl -Base url for endpoint. Defaults to https://firebasevertexai.googleapis.com +Base url for endpoint. Defaults to https://firebasevertexai.googleapis.com, which is the [Firebase AI Logic API](https://console.cloud.google.com/apis/library/firebasevertexai.googleapis.com?project=_) (used regardless of your chosen Gemini API provider). Signature: diff --git a/docs-devsite/ai.schema.md b/docs-devsite/ai.schema.md index fa1225c91e5..a6301259080 100644 --- a/docs-devsite/ai.schema.md +++ b/docs-devsite/ai.schema.md @@ -33,15 +33,16 @@ export declare abstract class Schema implements SchemaInterface | [example](./ai.schema.md#schemaexample) | | unknown | Optional. The example of the property. | | [format](./ai.schema.md#schemaformat) | | string | Optional. The format of the property. Supported formats:
  • for NUMBER type: "float", "double"
  • for INTEGER type: "int32", "int64"
  • for STRING type: "email", "byte", etc
| | [items](./ai.schema.md#schemaitems) | | [SchemaInterface](./ai.schemainterface.md#schemainterface_interface) | Optional. The items of the property. | -| [maxItems](./ai.schema.md#schemamaxitems) | | number | The maximum number of items (elements) in a schema of type [SchemaType.ARRAY](./ai.md#schematypearray_enummember). | -| [minItems](./ai.schema.md#schemaminitems) | | number | The minimum number of items (elements) in a schema of type [SchemaType.ARRAY](./ai.md#schematypearray_enummember). | +| [maxItems](./ai.schema.md#schemamaxitems) | | number | The maximum number of items (elements) in a schema of [SchemaType](./ai.md#schematype) array. | +| [minItems](./ai.schema.md#schemaminitems) | | number | The minimum number of items (elements) in a schema of [SchemaType](./ai.md#schematype) array. | | [nullable](./ai.schema.md#schemanullable) | | boolean | Optional. Whether the property is nullable. Defaults to false. | -| [type](./ai.schema.md#schematype) | | [SchemaType](./ai.md#schematype) | Optional. The type of the property. [SchemaType](./ai.md#schematype). | +| [type](./ai.schema.md#schematype) | | [SchemaType](./ai.md#schematype) | Optional. The type of the property. This can only be undefined when using anyOf schemas, which do not have an explicit type in the [OpenAPI specification](https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type). | ## Methods | Method | Modifiers | Description | | --- | --- | --- | +| [anyOf(anyOfParams)](./ai.schema.md#schemaanyof) | static | | | [array(arrayParams)](./ai.schema.md#schemaarray) | static | | | [boolean(booleanParams)](./ai.schema.md#schemaboolean) | static | | | [enumString(stringParams)](./ai.schema.md#schemaenumstring) | static | | @@ -108,7 +109,7 @@ items?: SchemaInterface; ## Schema.maxItems -The maximum number of items (elements) in a schema of type [SchemaType.ARRAY](./ai.md#schematypearray_enummember). +The maximum number of items (elements) in a schema of [SchemaType](./ai.md#schematype) `array`. Signature: @@ -118,7 +119,7 @@ maxItems?: number; ## Schema.minItems -The minimum number of items (elements) in a schema of type [SchemaType.ARRAY](./ai.md#schematypearray_enummember). +The minimum number of items (elements) in a schema of [SchemaType](./ai.md#schematype) `array`. Signature: @@ -138,14 +139,34 @@ nullable: boolean; ## Schema.type -Optional. The type of the property. [SchemaType](./ai.md#schematype). +Optional. The type of the property. This can only be undefined when using `anyOf` schemas, which do not have an explicit type in the [OpenAPI specification](https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type). Signature: ```typescript -type: SchemaType; +type?: SchemaType; ``` +## Schema.anyOf() + +Signature: + +```typescript +static anyOf(anyOfParams: SchemaParams & { + anyOf: TypedSchema[]; + }): AnyOfSchema; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| anyOfParams | [SchemaParams](./ai.schemaparams.md#schemaparams_interface) & { anyOf: [TypedSchema](./ai.md#typedschema)\[\]; } | | + +Returns: + +[AnyOfSchema](./ai.anyofschema.md#anyofschema_class) + ## Schema.array() Signature: diff --git a/docs-devsite/ai.schemainterface.md b/docs-devsite/ai.schemainterface.md index 6dd33e69e18..91429914ab7 100644 --- a/docs-devsite/ai.schemainterface.md +++ b/docs-devsite/ai.schemainterface.md @@ -23,14 +23,14 @@ export interface SchemaInterface extends SchemaShared | Property | Type | Description | | --- | --- | --- | -| [type](./ai.schemainterface.md#schemainterfacetype) | [SchemaType](./ai.md#schematype) | The type of the property. [SchemaType](./ai.md#schematype). | +| [type](./ai.schemainterface.md#schemainterfacetype) | [SchemaType](./ai.md#schematype) | The type of the property. this can only be undefined when using anyof schemas, which do not have an explicit type in the [OpenAPI Specification](https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type). | ## SchemaInterface.type -The type of the property. [SchemaType](./ai.md#schematype). +The type of the property. this can only be undefined when using `anyof` schemas, which do not have an explicit type in the [OpenAPI Specification](https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type). Signature: ```typescript -type: SchemaType; +type?: SchemaType; ``` diff --git a/docs-devsite/ai.schemarequest.md b/docs-devsite/ai.schemarequest.md index e71d24a6b1a..1eeb76fd8d1 100644 --- a/docs-devsite/ai.schemarequest.md +++ b/docs-devsite/ai.schemarequest.md @@ -24,7 +24,7 @@ export interface SchemaRequest extends SchemaShared | Property | Type | Description | | --- | --- | --- | | [required](./ai.schemarequest.md#schemarequestrequired) | string\[\] | Optional. Array of required property. | -| [type](./ai.schemarequest.md#schemarequesttype) | [SchemaType](./ai.md#schematype) | The type of the property. [SchemaType](./ai.md#schematype). | +| [type](./ai.schemarequest.md#schemarequesttype) | [SchemaType](./ai.md#schematype) | The type of the property. this can only be undefined when using anyOf schemas, which do not have an explicit type in the [OpenAPI specification](https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type). | ## SchemaRequest.required @@ -38,10 +38,10 @@ required?: string[]; ## SchemaRequest.type -The type of the property. [SchemaType](./ai.md#schematype). +The type of the property. this can only be undefined when using `anyOf` schemas, which do not have an explicit type in the [OpenAPI specification](https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type). Signature: ```typescript -type: SchemaType; +type?: SchemaType; ``` diff --git a/docs-devsite/ai.schemashared.md b/docs-devsite/ai.schemashared.md index fb75fc50841..205d33ed3dd 100644 --- a/docs-devsite/ai.schemashared.md +++ b/docs-devsite/ai.schemashared.md @@ -22,20 +22,31 @@ export interface SchemaShared | Property | Type | Description | | --- | --- | --- | +| [anyOf](./ai.schemashared.md#schemasharedanyof) | T\[\] | An array of [Schema](./ai.schema.md#schema_class). The generated data must be valid against any of the schemas listed in this array. This allows specifying multiple possible structures or types for a single field. | | [description](./ai.schemashared.md#schemashareddescription) | string | Optional. The description of the property. | | [enum](./ai.schemashared.md#schemasharedenum) | string\[\] | Optional. The enum of the property. | | [example](./ai.schemashared.md#schemasharedexample) | unknown | Optional. The example of the property. | | [format](./ai.schemashared.md#schemasharedformat) | string | Optional. The format of the property. When using the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)), this must be either 'enum' or 'date-time', otherwise requests will fail. | | [items](./ai.schemashared.md#schemashareditems) | T | Optional. The items of the property. | | [maximum](./ai.schemashared.md#schemasharedmaximum) | number | The maximum value of a numeric type. | -| [maxItems](./ai.schemashared.md#schemasharedmaxitems) | number | The maximum number of items (elements) in a schema of type [SchemaType.ARRAY](./ai.md#schematypearray_enummember). | +| [maxItems](./ai.schemashared.md#schemasharedmaxitems) | number | The maximum number of items (elements) in a schema of [SchemaType](./ai.md#schematype) array. | | [minimum](./ai.schemashared.md#schemasharedminimum) | number | The minimum value of a numeric type. | -| [minItems](./ai.schemashared.md#schemasharedminitems) | number | The minimum number of items (elements) in a schema of type [SchemaType.ARRAY](./ai.md#schematypearray_enummember). | +| [minItems](./ai.schemashared.md#schemasharedminitems) | number | The minimum number of items (elements) in a schema of [SchemaType](./ai.md#schematype) array. | | [nullable](./ai.schemashared.md#schemasharednullable) | boolean | Optional. Whether the property is nullable. | | [properties](./ai.schemashared.md#schemasharedproperties) | { \[k: string\]: T; } | Optional. Map of Schema objects. | | [propertyOrdering](./ai.schemashared.md#schemasharedpropertyordering) | string\[\] | A hint suggesting the order in which the keys should appear in the generated JSON string. | | [title](./ai.schemashared.md#schemasharedtitle) | string | The title of the property. This helps document the schema's purpose but does not typically constrain the generated value. It can subtly guide the model by clarifying the intent of a field. | +## SchemaShared.anyOf + +An array of [Schema](./ai.schema.md#schema_class). The generated data must be valid against any of the schemas listed in this array. This allows specifying multiple possible structures or types for a single field. + +Signature: + +```typescript +anyOf?: T[]; +``` + ## SchemaShared.description Optional. The description of the property. @@ -98,7 +109,7 @@ maximum?: number; ## SchemaShared.maxItems -The maximum number of items (elements) in a schema of type [SchemaType.ARRAY](./ai.md#schematypearray_enummember). +The maximum number of items (elements) in a schema of [SchemaType](./ai.md#schematype) `array`. Signature: @@ -118,7 +129,7 @@ minimum?: number; ## SchemaShared.minItems -The minimum number of items (elements) in a schema of type [SchemaType.ARRAY](./ai.md#schematypearray_enummember). +The minimum number of items (elements) in a schema of [SchemaType](./ai.md#schematype) `array`. Signature: diff --git a/docs-devsite/ai.searchentrypoint.md b/docs-devsite/ai.searchentrypoint.md new file mode 100644 index 00000000000..db35db06a49 --- /dev/null +++ b/docs-devsite/ai.searchentrypoint.md @@ -0,0 +1,48 @@ +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 %} + +# SearchEntrypoint interface +Google search entry point. + +Signature: + +```typescript +export interface SearchEntrypoint +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [renderedContent](./ai.searchentrypoint.md#searchentrypointrenderedcontent) | string | HTML/CSS snippet that must be embedded in a web page. The snippet is designed to avoid undesired interaction with the rest of the page's CSS.To ensure proper rendering and prevent CSS conflicts, it is recommended to encapsulate this renderedContent within a shadow DOM when embedding it into a webpage. See [MDN: Using shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). | + +## SearchEntrypoint.renderedContent + +HTML/CSS snippet that must be embedded in a web page. The snippet is designed to avoid undesired interaction with the rest of the page's CSS. + +To ensure proper rendering and prevent CSS conflicts, it is recommended to encapsulate this `renderedContent` within a shadow DOM when embedding it into a webpage. See [MDN: Using shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). + +Signature: + +```typescript +renderedContent?: string; +``` + +### Example + + +```javascript +const container = document.createElement('div'); +document.body.appendChild(container); +container.attachShadow({ mode: 'open' }).innerHTML = renderedContent; + +``` + diff --git a/docs-devsite/ai.segment.md b/docs-devsite/ai.segment.md index 69f4aaf8407..35db1be5e83 100644 --- a/docs-devsite/ai.segment.md +++ b/docs-devsite/ai.segment.md @@ -10,6 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # Segment interface +Represents a specific segment within a [Content](./ai.content.md#content_interface) object, often used to pinpoint the exact location of text or data that grounding information refers to. Signature: @@ -21,12 +22,15 @@ export interface Segment | Property | Type | Description | | --- | --- | --- | -| [endIndex](./ai.segment.md#segmentendindex) | number | | -| [partIndex](./ai.segment.md#segmentpartindex) | number | | -| [startIndex](./ai.segment.md#segmentstartindex) | number | | +| [endIndex](./ai.segment.md#segmentendindex) | number | The zero-based end index of the segment within the specified Part, measured in UTF-8 bytes. This offset is exclusive, meaning the character at this index is not included in the segment. | +| [partIndex](./ai.segment.md#segmentpartindex) | number | The zero-based index of the [Part](./ai.md#part) object within the parts array of its parent [Content](./ai.content.md#content_interface) object. This identifies which part of the content the segment belongs to. | +| [startIndex](./ai.segment.md#segmentstartindex) | number | The zero-based start index of the segment within the specified Part, measured in UTF-8 bytes. This offset is inclusive, starting from 0 at the beginning of the part's content (e.g., Part.text). | +| [text](./ai.segment.md#segmenttext) | string | The text corresponding to the segment from the response. | ## Segment.endIndex +The zero-based end index of the segment within the specified `Part`, measured in UTF-8 bytes. This offset is exclusive, meaning the character at this index is not included in the segment. + Signature: ```typescript @@ -35,6 +39,8 @@ endIndex: number; ## Segment.partIndex +The zero-based index of the [Part](./ai.md#part) object within the `parts` array of its parent [Content](./ai.content.md#content_interface) object. This identifies which part of the content the segment belongs to. + Signature: ```typescript @@ -43,8 +49,20 @@ partIndex: number; ## Segment.startIndex +The zero-based start index of the segment within the specified `Part`, measured in UTF-8 bytes. This offset is inclusive, starting from 0 at the beginning of the part's content (e.g., `Part.text`). + Signature: ```typescript startIndex: number; ``` + +## Segment.text + +The text corresponding to the segment from the response. + +Signature: + +```typescript +text: string; +``` diff --git a/docs-devsite/ai.speechconfig.md b/docs-devsite/ai.speechconfig.md new file mode 100644 index 00000000000..95c63964974 --- /dev/null +++ b/docs-devsite/ai.speechconfig.md @@ -0,0 +1,41 @@ +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 %} + +# SpeechConfig interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configures speech synthesis. + +Signature: + +```typescript +export interface SpeechConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [voiceConfig](./ai.speechconfig.md#speechconfigvoiceconfig) | [VoiceConfig](./ai.voiceconfig.md#voiceconfig_interface) | (Public Preview) Configures the voice to be used in speech synthesis. | + +## SpeechConfig.voiceConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configures the voice to be used in speech synthesis. + +Signature: + +```typescript +voiceConfig?: VoiceConfig; +``` diff --git a/docs-devsite/ai.startaudioconversationoptions.md b/docs-devsite/ai.startaudioconversationoptions.md new file mode 100644 index 00000000000..827cc0b129b --- /dev/null +++ b/docs-devsite/ai.startaudioconversationoptions.md @@ -0,0 +1,41 @@ +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 %} + +# StartAudioConversationOptions interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Options for [startAudioConversation()](./ai.md#startaudioconversation_01c8e7f). + +Signature: + +```typescript +export interface StartAudioConversationOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [functionCallingHandler](./ai.startaudioconversationoptions.md#startaudioconversationoptionsfunctioncallinghandler) | (functionCalls: [FunctionCall](./ai.functioncall.md#functioncall_interface)\[\]) => Promise<[FunctionResponse](./ai.functionresponse.md#functionresponse_interface)> | (Public Preview) An async handler that is called when the model requests a function to be executed. The handler should perform the function call and return the result as a Part, which will then be sent back to the model. | + +## StartAudioConversationOptions.functionCallingHandler + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +An async handler that is called when the model requests a function to be executed. The handler should perform the function call and return the result as a `Part`, which will then be sent back to the model. + +Signature: + +```typescript +functionCallingHandler?: (functionCalls: FunctionCall[]) => Promise; +``` diff --git a/docs-devsite/ai.textpart.md b/docs-devsite/ai.textpart.md index 2057d95d32e..22236b37d7d 100644 --- a/docs-devsite/ai.textpart.md +++ b/docs-devsite/ai.textpart.md @@ -22,10 +22,29 @@ export interface TextPart | Property | Type | Description | | --- | --- | --- | +| [codeExecutionResult](./ai.textpart.md#textpartcodeexecutionresult) | never | | +| [executableCode](./ai.textpart.md#textpartexecutablecode) | never | | | [functionCall](./ai.textpart.md#textpartfunctioncall) | never | | | [functionResponse](./ai.textpart.md#textpartfunctionresponse) | never | | | [inlineData](./ai.textpart.md#textpartinlinedata) | never | | | [text](./ai.textpart.md#textparttext) | string | | +| [thought](./ai.textpart.md#textpartthought) | boolean | | + +## TextPart.codeExecutionResult + +Signature: + +```typescript +codeExecutionResult?: never; +``` + +## TextPart.executableCode + +Signature: + +```typescript +executableCode?: never; +``` ## TextPart.functionCall @@ -58,3 +77,11 @@ inlineData?: never; ```typescript text: string; ``` + +## TextPart.thought + +Signature: + +```typescript +thought?: boolean; +``` diff --git a/docs-devsite/ai.thinkingconfig.md b/docs-devsite/ai.thinkingconfig.md new file mode 100644 index 00000000000..1ddc1626f48 --- /dev/null +++ b/docs-devsite/ai.thinkingconfig.md @@ -0,0 +1,56 @@ +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 %} + +# ThinkingConfig interface +Configuration for "thinking" behavior of compatible Gemini models. + +Certain models utilize a thinking process before generating a response. This allows them to reason through complex problems and plan a more coherent and accurate answer. + +Signature: + +```typescript +export interface ThinkingConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [includeThoughts](./ai.thinkingconfig.md#thinkingconfigincludethoughts) | boolean | Whether to include "thought summaries" in the model's response. | +| [thinkingBudget](./ai.thinkingconfig.md#thinkingconfigthinkingbudget) | number | The thinking budget, in tokens.This parameter sets an upper limit on the number of tokens the model can use for its internal "thinking" process. A higher budget may result in higher quality responses for complex tasks but can also increase latency and cost.If you don't specify a budget, the model will determine the appropriate amount of thinking based on the complexity of the prompt.An error will be thrown if you set a thinking budget for a model that does not support this feature or if the specified budget is not within the model's supported range. | + +## ThinkingConfig.includeThoughts + +Whether to include "thought summaries" in the model's response. + +Thought summaries provide a brief overview of the model's internal thinking process, offering insight into how it arrived at the final answer. This can be useful for debugging, understanding the model's reasoning, and verifying its accuracy. + +Signature: + +```typescript +includeThoughts?: boolean; +``` + +## ThinkingConfig.thinkingBudget + +The thinking budget, in tokens. + +This parameter sets an upper limit on the number of tokens the model can use for its internal "thinking" process. A higher budget may result in higher quality responses for complex tasks but can also increase latency and cost. + +If you don't specify a budget, the model will determine the appropriate amount of thinking based on the complexity of the prompt. + +An error will be thrown if you set a thinking budget for a model that does not support this feature or if the specified budget is not within the model's supported range. + +Signature: + +```typescript +thinkingBudget?: number; +``` diff --git a/docs-devsite/ai.urlcontext.md b/docs-devsite/ai.urlcontext.md new file mode 100644 index 00000000000..435d278e4d1 --- /dev/null +++ b/docs-devsite/ai.urlcontext.md @@ -0,0 +1,22 @@ +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 %} + +# URLContext interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Specifies the URL Context configuration. + +Signature: + +```typescript +export interface URLContext +``` diff --git a/docs-devsite/ai.urlcontextmetadata.md b/docs-devsite/ai.urlcontextmetadata.md new file mode 100644 index 00000000000..bc260b997ad --- /dev/null +++ b/docs-devsite/ai.urlcontextmetadata.md @@ -0,0 +1,41 @@ +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 %} + +# URLContextMetadata interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Metadata related to [URLContextTool](./ai.urlcontexttool.md#urlcontexttool_interface). + +Signature: + +```typescript +export interface URLContextMetadata +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [urlMetadata](./ai.urlcontextmetadata.md#urlcontextmetadataurlmetadata) | [URLMetadata](./ai.urlmetadata.md#urlmetadata_interface)\[\] | (Public Preview) List of URL metadata used to provide context to the Gemini model. | + +## URLContextMetadata.urlMetadata + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +List of URL metadata used to provide context to the Gemini model. + +Signature: + +```typescript +urlMetadata: URLMetadata[]; +``` diff --git a/docs-devsite/ai.urlcontexttool.md b/docs-devsite/ai.urlcontexttool.md new file mode 100644 index 00000000000..6ecc2a323c1 --- /dev/null +++ b/docs-devsite/ai.urlcontexttool.md @@ -0,0 +1,41 @@ +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 %} + +# URLContextTool interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +A tool that allows you to provide additional context to the models in the form of public web URLs. By including URLs in your request, the Gemini model will access the content from those pages to inform and enhance its response. + +Signature: + +```typescript +export interface URLContextTool +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [urlContext](./ai.urlcontexttool.md#urlcontexttoolurlcontext) | [URLContext](./ai.urlcontext.md#urlcontext_interface) | (Public Preview) Specifies the URL Context configuration. | + +## URLContextTool.urlContext + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Specifies the URL Context configuration. + +Signature: + +```typescript +urlContext: URLContext; +``` diff --git a/docs-devsite/ai.urlmetadata.md b/docs-devsite/ai.urlmetadata.md new file mode 100644 index 00000000000..3cbd27632d0 --- /dev/null +++ b/docs-devsite/ai.urlmetadata.md @@ -0,0 +1,55 @@ +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 %} + +# URLMetadata interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Metadata for a single URL retrieved by the [URLContextTool](./ai.urlcontexttool.md#urlcontexttool_interface) tool. + +Signature: + +```typescript +export interface URLMetadata +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [retrievedUrl](./ai.urlmetadata.md#urlmetadataretrievedurl) | string | (Public Preview) The retrieved URL. | +| [urlRetrievalStatus](./ai.urlmetadata.md#urlmetadataurlretrievalstatus) | [URLRetrievalStatus](./ai.md#urlretrievalstatus) | (Public Preview) The status of the URL retrieval. | + +## URLMetadata.retrievedUrl + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The retrieved URL. + +Signature: + +```typescript +retrievedUrl?: string; +``` + +## URLMetadata.urlRetrievalStatus + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The status of the URL retrieval. + +Signature: + +```typescript +urlRetrievalStatus?: URLRetrievalStatus; +``` diff --git a/docs-devsite/ai.usagemetadata.md b/docs-devsite/ai.usagemetadata.md index 4211fea72b4..bf45610f4a1 100644 --- a/docs-devsite/ai.usagemetadata.md +++ b/docs-devsite/ai.usagemetadata.md @@ -26,6 +26,9 @@ export interface UsageMetadata | [candidatesTokensDetails](./ai.usagemetadata.md#usagemetadatacandidatestokensdetails) | [ModalityTokenCount](./ai.modalitytokencount.md#modalitytokencount_interface)\[\] | | | [promptTokenCount](./ai.usagemetadata.md#usagemetadataprompttokencount) | number | | | [promptTokensDetails](./ai.usagemetadata.md#usagemetadataprompttokensdetails) | [ModalityTokenCount](./ai.modalitytokencount.md#modalitytokencount_interface)\[\] | | +| [thoughtsTokenCount](./ai.usagemetadata.md#usagemetadatathoughtstokencount) | number | The number of tokens used by the model's internal "thinking" process. | +| [toolUsePromptTokenCount](./ai.usagemetadata.md#usagemetadatatooluseprompttokencount) | number | The number of tokens used by tools. | +| [toolUsePromptTokensDetails](./ai.usagemetadata.md#usagemetadatatooluseprompttokensdetails) | [ModalityTokenCount](./ai.modalitytokencount.md#modalitytokencount_interface)\[\] | A list of tokens used by tools, broken down by modality. | | [totalTokenCount](./ai.usagemetadata.md#usagemetadatatotaltokencount) | number | | ## UsageMetadata.candidatesTokenCount @@ -60,6 +63,36 @@ promptTokenCount: number; promptTokensDetails?: ModalityTokenCount[]; ``` +## UsageMetadata.thoughtsTokenCount + +The number of tokens used by the model's internal "thinking" process. + +Signature: + +```typescript +thoughtsTokenCount?: number; +``` + +## UsageMetadata.toolUsePromptTokenCount + +The number of tokens used by tools. + +Signature: + +```typescript +toolUsePromptTokenCount?: number; +``` + +## UsageMetadata.toolUsePromptTokensDetails + +A list of tokens used by tools, broken down by modality. + +Signature: + +```typescript +toolUsePromptTokensDetails?: ModalityTokenCount[]; +``` + ## UsageMetadata.totalTokenCount Signature: diff --git a/docs-devsite/ai.voiceconfig.md b/docs-devsite/ai.voiceconfig.md new file mode 100644 index 00000000000..b22ac7e104c --- /dev/null +++ b/docs-devsite/ai.voiceconfig.md @@ -0,0 +1,41 @@ +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 %} + +# VoiceConfig interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configuration for the voice to used in speech synthesis. + +Signature: + +```typescript +export interface VoiceConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [prebuiltVoiceConfig](./ai.voiceconfig.md#voiceconfigprebuiltvoiceconfig) | [PrebuiltVoiceConfig](./ai.prebuiltvoiceconfig.md#prebuiltvoiceconfig_interface) | (Public Preview) Configures the voice using a pre-built voice configuration. | + +## VoiceConfig.prebuiltVoiceConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configures the voice using a pre-built voice configuration. + +Signature: + +```typescript +prebuiltVoiceConfig?: PrebuiltVoiceConfig; +``` diff --git a/docs-devsite/ai.webgroundingchunk.md b/docs-devsite/ai.webgroundingchunk.md new file mode 100644 index 00000000000..8d4c59f7e23 --- /dev/null +++ b/docs-devsite/ai.webgroundingchunk.md @@ -0,0 +1,61 @@ +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 %} + +# WebGroundingChunk interface +A grounding chunk from the web. + +Important: If using Grounding with Google Search, you are required to comply with the [Service Specific Terms](https://cloud.google.com/terms/service-terms) for "Grounding with Google Search". + +Signature: + +```typescript +export interface WebGroundingChunk +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [domain](./ai.webgroundingchunk.md#webgroundingchunkdomain) | string | The domain of the original URI from which the content was retrieved.This property is only supported in the Vertex AI Gemini API ([VertexAIBackend](./ai.vertexaibackend.md#vertexaibackend_class)). When using the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)), this property will be undefined. | +| [title](./ai.webgroundingchunk.md#webgroundingchunktitle) | string | The title of the retrieved web page. | +| [uri](./ai.webgroundingchunk.md#webgroundingchunkuri) | string | The URI of the retrieved web page. | + +## WebGroundingChunk.domain + +The domain of the original URI from which the content was retrieved. + +This property is only supported in the Vertex AI Gemini API ([VertexAIBackend](./ai.vertexaibackend.md#vertexaibackend_class)). When using the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)), this property will be `undefined`. + +Signature: + +```typescript +domain?: string; +``` + +## WebGroundingChunk.title + +The title of the retrieved web page. + +Signature: + +```typescript +title?: string; +``` + +## WebGroundingChunk.uri + +The URI of the retrieved web page. + +Signature: + +```typescript +uri?: string; +``` diff --git a/docs-devsite/app.md b/docs-devsite/app.md index 9c3b322aaaf..3306d5fc5d9 100644 --- a/docs-devsite/app.md +++ b/docs-devsite/app.md @@ -23,6 +23,8 @@ This package coordinates the communication between the different Firebase compon | function() | | [getApps()](./app.md#getapps) | A (read-only) array of all initialized apps. | | [initializeApp()](./app.md#initializeapp) | Creates and initializes a FirebaseApp instance. | +| function(config, ...) | +| [initializeServerApp(config)](./app.md#initializeserverapp_e7d0728) | Creates and initializes a [FirebaseServerApp](./app.firebaseserverapp.md#firebaseserverapp_interface) instance. | | function(libraryKeyOrName, ...) | | [registerVersion(libraryKeyOrName, version, variant)](./app.md#registerversion_f673248) | Registers a library's name and version for platform logging purposes. | | function(logCallback, ...) | @@ -116,6 +118,38 @@ export declare function initializeApp(): FirebaseApp; [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) +## function(config, ...) + +### initializeServerApp(config) {:#initializeserverapp_e7d0728} + +Creates and initializes a [FirebaseServerApp](./app.firebaseserverapp.md#firebaseserverapp_interface) instance. + +Signature: + +```typescript +export declare function initializeServerApp(config?: FirebaseServerAppSettings): FirebaseServerApp; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| config | [FirebaseServerAppSettings](./app.firebaseserverappsettings.md#firebaseserverappsettings_interface) | Optional FirebaseServerApp settings. | + +Returns: + +[FirebaseServerApp](./app.firebaseserverapp.md#firebaseserverapp_interface) + +The initialized `FirebaseServerApp`. + +#### Exceptions + +If invoked in an unsupported non-server environment such as a browser. + +If [FirebaseServerAppSettings.releaseOnDeref](./app.firebaseserverappsettings.md#firebaseserverappsettingsreleaseonderef) is defined but the runtime doesn't provide Finalization Registry support. + +If the `FIREBASE_OPTIONS` environment variable does not contain a valid project configuration required for auto-initialization. + ## function(libraryKeyOrName, ...) ### registerVersion(libraryKeyOrName, version, variant) {:#registerversion_f673248} @@ -260,6 +294,12 @@ export declare function initializeApp(options: FirebaseOptions, name?: string): The initialized app. +#### Exceptions + +If the optional `name` parameter is malformed or empty. + +If a `FirebaseApp` already exists with the same name but with a different configuration. + ### Example 1 @@ -312,6 +352,12 @@ export declare function initializeApp(options: FirebaseOptions, config?: Firebas [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) +#### Exceptions + +If [FirebaseAppSettings.name](./app.firebaseappsettings.md#firebaseappsettingsname) is defined but the value is malformed or empty. + +If a `FirebaseApp` already exists with the same name but with a different configuration. + ### initializeServerApp(options, config) {:#initializeserverapp_30ab697} Creates and initializes a [FirebaseServerApp](./app.firebaseserverapp.md#firebaseserverapp_interface) instance. @@ -323,7 +369,7 @@ See [Add Firebase to your app](https://firebase.google.com/docs/web/setup#add_fi Signature: ```typescript -export declare function initializeServerApp(options: FirebaseOptions | FirebaseApp, config: FirebaseServerAppSettings): FirebaseServerApp; +export declare function initializeServerApp(options: FirebaseOptions | FirebaseApp, config?: FirebaseServerAppSettings): FirebaseServerApp; ``` #### Parameters @@ -331,7 +377,7 @@ export declare function initializeServerApp(options: FirebaseOptions | FirebaseA | Parameter | Type | Description | | --- | --- | --- | | options | [FirebaseOptions](./app.firebaseoptions.md#firebaseoptions_interface) \| [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) | Firebase.AppOptions to configure the app's services, or a a FirebaseApp instance which contains the AppOptions within. | -| config | [FirebaseServerAppSettings](./app.firebaseserverappsettings.md#firebaseserverappsettings_interface) | FirebaseServerApp configuration. | +| config | [FirebaseServerAppSettings](./app.firebaseserverappsettings.md#firebaseserverappsettings_interface) | Optional FirebaseServerApp settings. | Returns: @@ -339,6 +385,12 @@ export declare function initializeServerApp(options: FirebaseOptions | FirebaseA The initialized `FirebaseServerApp`. +#### Exceptions + +If invoked in an unsupported non-server environment such as a browser. + +If [FirebaseServerAppSettings.releaseOnDeref](./app.firebaseserverappsettings.md#firebaseserverappsettingsreleaseonderef) is defined but the runtime doesn't provide Finalization Registry support. + ### Example diff --git a/docs-devsite/remote-config.configupdate.md b/docs-devsite/remote-config.configupdate.md new file mode 100644 index 00000000000..231c8b1eb1f --- /dev/null +++ b/docs-devsite/remote-config.configupdate.md @@ -0,0 +1,39 @@ +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 %} + +# ConfigUpdate interface +Contains information about which keys have been updated. + +Signature: + +```typescript +export interface ConfigUpdate +``` + +## Methods + +| Method | Description | +| --- | --- | +| [getUpdatedKeys()](./remote-config.configupdate.md#configupdategetupdatedkeys) | Parameter keys whose values have been updated from the currently activated values. Includes keys that are added, deleted, or whose value, value source, or metadata has changed. | + +## ConfigUpdate.getUpdatedKeys() + +Parameter keys whose values have been updated from the currently activated values. Includes keys that are added, deleted, or whose value, value source, or metadata has changed. + +Signature: + +```typescript +getUpdatedKeys(): Set; +``` +Returns: + +Set<string> + diff --git a/docs-devsite/remote-config.configupdateobserver.md b/docs-devsite/remote-config.configupdateobserver.md new file mode 100644 index 00000000000..93f9154bb91 --- /dev/null +++ b/docs-devsite/remote-config.configupdateobserver.md @@ -0,0 +1,59 @@ +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 %} + +# ConfigUpdateObserver interface +Observer interface for receiving real-time Remote Config update notifications. + +NOTE: Although an `complete` callback can be provided, it will never be called because the ConfigUpdate stream is never-ending. + +Signature: + +```typescript +export interface ConfigUpdateObserver +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [complete](./remote-config.configupdateobserver.md#configupdateobservercomplete) | () => void | Called when the stream is gracefully terminated. | +| [error](./remote-config.configupdateobserver.md#configupdateobservererror) | (error: [FirebaseError](./util.firebaseerror.md#firebaseerror_class)) => void | Called if an error occurs during the stream. | +| [next](./remote-config.configupdateobserver.md#configupdateobservernext) | (configUpdate: [ConfigUpdate](./remote-config.configupdate.md#configupdate_interface)) => void | Called when a new ConfigUpdate is available. | + +## ConfigUpdateObserver.complete + +Called when the stream is gracefully terminated. + +Signature: + +```typescript +complete: () => void; +``` + +## ConfigUpdateObserver.error + +Called if an error occurs during the stream. + +Signature: + +```typescript +error: (error: FirebaseError) => void; +``` + +## ConfigUpdateObserver.next + +Called when a new ConfigUpdate is available. + +Signature: + +```typescript +next: (configUpdate: ConfigUpdate) => void; +``` diff --git a/docs-devsite/remote-config.fetchresponse.md b/docs-devsite/remote-config.fetchresponse.md index 414188e72bb..1955dd47492 100644 --- a/docs-devsite/remote-config.fetchresponse.md +++ b/docs-devsite/remote-config.fetchresponse.md @@ -27,6 +27,7 @@ export interface FetchResponse | [config](./remote-config.fetchresponse.md#fetchresponseconfig) | [FirebaseRemoteConfigObject](./remote-config.firebaseremoteconfigobject.md#firebaseremoteconfigobject_interface) | Defines the map of parameters returned as "entries" in the fetch response body.

Only defined for 200 responses. | | [eTag](./remote-config.fetchresponse.md#fetchresponseetag) | string | Defines the ETag response header value.

Only defined for 200 and 304 responses. | | [status](./remote-config.fetchresponse.md#fetchresponsestatus) | number | The HTTP status, which is useful for differentiating success responses with data from those without.

The Remote Config client is modeled after the native Fetch interface, so HTTP status is first-class.

Disambiguation: the fetch response returns a legacy "state" value that is redundant with the HTTP status code. The former is normalized into the latter. | +| [templateVersion](./remote-config.fetchresponse.md#fetchresponsetemplateversion) | number | The version number of the config template fetched from the server. | ## FetchResponse.config @@ -65,3 +66,13 @@ The HTTP status, which is useful for differentiating success responses with data ```typescript status: number; ``` + +## FetchResponse.templateVersion + +The version number of the config template fetched from the server. + +Signature: + +```typescript +templateVersion?: number; +``` diff --git a/docs-devsite/remote-config.md b/docs-devsite/remote-config.md index 58d23cfd647..c9f803abf16 100644 --- a/docs-devsite/remote-config.md +++ b/docs-devsite/remote-config.md @@ -28,6 +28,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm | [getNumber(remoteConfig, key)](./remote-config.md#getnumber_476c09f) | Gets the value for the given key as a number.Convenience method for calling remoteConfig.getValue(key).asNumber(). | | [getString(remoteConfig, key)](./remote-config.md#getstring_476c09f) | Gets the value for the given key as a string. Convenience method for calling remoteConfig.getValue(key).asString(). | | [getValue(remoteConfig, key)](./remote-config.md#getvalue_476c09f) | Gets the [Value](./remote-config.value.md#value_interface) for the given key. | +| [onConfigUpdate(remoteConfig, observer)](./remote-config.md#onconfigupdate_8b13b26) | Starts listening for real-time config updates from the Remote Config backend and automatically fetches updates from the Remote Config backend when they are available. | | [setCustomSignals(remoteConfig, customSignals)](./remote-config.md#setcustomsignals_aeeb95e) | Sets the custom signals for the app instance. | | [setLogLevel(remoteConfig, logLevel)](./remote-config.md#setloglevel_039a45b) | Defines the log level to use. | | function() | @@ -37,6 +38,8 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm | Interface | Description | | --- | --- | +| [ConfigUpdate](./remote-config.configupdate.md#configupdate_interface) | Contains information about which keys have been updated. | +| [ConfigUpdateObserver](./remote-config.configupdateobserver.md#configupdateobserver_interface) | Observer interface for receiving real-time Remote Config update notifications.NOTE: Although an complete callback can be provided, it will never be called because the ConfigUpdate stream is never-ending. | | [CustomSignals](./remote-config.customsignals.md#customsignals_interface) | Defines the type for representing custom signals and their values.

The values in CustomSignals must be one of the following types:

  • string
  • number
  • null
| | [FetchResponse](./remote-config.fetchresponse.md#fetchresponse_interface) | Defines a successful response (200 or 304).

Modeled after the native Response interface, but simplified for Remote Config's use case. | | [FirebaseRemoteConfigObject](./remote-config.firebaseremoteconfigobject.md#firebaseremoteconfigobject_interface) | Defines a self-descriptive reference for config key-value pairs. | @@ -50,7 +53,9 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm | Type Alias | Description | | --- | --- | | [FetchStatus](./remote-config.md#fetchstatus) | Summarizes the outcome of the last attempt to fetch config from the Firebase Remote Config server.

  • "no-fetch-yet" indicates the [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) instance has not yet attempted to fetch config, or that SDK initialization is incomplete.
  • "success" indicates the last attempt succeeded.
  • "failure" indicates the last attempt failed.
  • "throttle" indicates the last attempt was rate-limited.
| +| [FetchType](./remote-config.md#fetchtype) | Indicates the type of fetch request.
  • "BASE" indicates a standard fetch request.
  • "REALTIME" indicates a fetch request triggered by a real-time update.
| | [LogLevel](./remote-config.md#loglevel) | Defines levels of Remote Config logging. | +| [Unsubscribe](./remote-config.md#unsubscribe) | A function that unsubscribes from a real-time event stream. | | [ValueSource](./remote-config.md#valuesource) | Indicates the source of a value.
  • "static" indicates the value was defined by a static constant.
  • "default" indicates the value was defined by default config.
  • "remote" indicates the value was defined by fetched config.
| ## function(app, ...) @@ -282,6 +287,31 @@ export declare function getValue(remoteConfig: RemoteConfig, key: string): Value The value for the given key. +### onConfigUpdate(remoteConfig, observer) {:#onconfigupdate_8b13b26} + +Starts listening for real-time config updates from the Remote Config backend and automatically fetches updates from the Remote Config backend when they are available. + +If a connection to the Remote Config backend is not already open, calling this method will open it. Multiple listeners can be added by calling this method again, but subsequent calls re-use the same connection to the backend. + +Signature: + +```typescript +export declare function onConfigUpdate(remoteConfig: RemoteConfig, observer: ConfigUpdateObserver): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| remoteConfig | [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) | The [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) instance. | +| observer | [ConfigUpdateObserver](./remote-config.configupdateobserver.md#configupdateobserver_interface) | The [ConfigUpdateObserver](./remote-config.configupdateobserver.md#configupdateobserver_interface) to be notified of config updates. | + +Returns: + +[Unsubscribe](./remote-config.md#unsubscribe) + +An [Unsubscribe](./remote-config.md#unsubscribe) function to remove the listener. + ### setCustomSignals(remoteConfig, customSignals) {:#setcustomsignals_aeeb95e} Sets the custom signals for the app instance. @@ -355,6 +385,18 @@ Summarizes the outcome of the last attempt to fetch config from the Firebase Rem export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle'; ``` +## FetchType + +Indicates the type of fetch request. + +
  • "BASE" indicates a standard fetch request.
  • "REALTIME" indicates a fetch request triggered by a real-time update.
+ +Signature: + +```typescript +export type FetchType = 'BASE' | 'REALTIME'; +``` + ## LogLevel Defines levels of Remote Config logging. @@ -365,6 +407,16 @@ Defines levels of Remote Config logging. export type LogLevel = 'debug' | 'error' | 'silent'; ``` +## Unsubscribe + +A function that unsubscribes from a real-time event stream. + +Signature: + +```typescript +export type Unsubscribe = () => void; +``` + ## ValueSource Indicates the source of a value. diff --git a/e2e/babel.config.js b/e2e/babel.config.js deleted file mode 100644 index f588a39a23d..00000000000 --- a/e2e/babel.config.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module.exports = { - presets: [ - ['@babel/preset-env', { targets: { node: 'current' } }], - '@babel/preset-typescript' - ] -}; diff --git a/e2e/data-connect/dataconnect-generated/js/default-connector/package.json b/e2e/data-connect/dataconnect-generated/js/default-connector/package.json index d0c9852ce3e..e9b57fa0a38 100644 --- a/e2e/data-connect/dataconnect-generated/js/default-connector/package.json +++ b/e2e/data-connect/dataconnect-generated/js/default-connector/package.json @@ -5,7 +5,7 @@ "description": "Generated SDK For default", "license": "Apache-2.0", "engines": { - "node": " >=18.0" + "node": " >=22.0.0" }, "typings": "index.d.ts", "module": "esm/index.esm.js", diff --git a/e2e/fix-jsdom-environment.ts b/e2e/fix-jsdom-environment.ts deleted file mode 100644 index c2885ab9c45..00000000000 --- a/e2e/fix-jsdom-environment.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import JSDOMEnvironment from 'jest-environment-jsdom'; - -/** - * JSDOMEnvironment patch to polyfill missing APIs with Node APIs. - */ -// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string -export default class FixJSDOMEnvironment extends JSDOMEnvironment { - constructor(...args: ConstructorParameters) { - super(...args); - - // Fetch - // FIXME: https://github.com/jsdom/jsdom/issues/1724 - this.global.fetch = fetch; - this.global.Headers = Headers; - this.global.Request = Request; - this.global.Response = Response; - - // Util - this.global.TextEncoder = TextEncoder; - } -} diff --git a/e2e/smoke-tests/package.json b/e2e/smoke-tests/package.json index ca33e7f696f..c957f018b5a 100644 --- a/e2e/smoke-tests/package.json +++ b/e2e/smoke-tests/package.json @@ -35,6 +35,6 @@ "webpack-dev-server": "5.2.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/e2e/smoke-tests/sample-apps/modular.js b/e2e/smoke-tests/sample-apps/modular.js index 2d66b752081..20ff0ce7bd2 100644 --- a/e2e/smoke-tests/sample-apps/modular.js +++ b/e2e/smoke-tests/sample-apps/modular.js @@ -313,7 +313,7 @@ function callPerformance(app) { async function callAI(app) { console.log('[AI] start'); const ai = getAI(app, { backend: new VertexAIBackend() }); - const model = getGenerativeModel(ai, { model: 'gemini-1.5-flash' }); + const model = getGenerativeModel(ai, { model: 'gemini-2.5-flash' }); const result = await model.countTokens('abcdefg'); console.log(`[AI] counted tokens: ${result.totalTokens}`); } diff --git a/e2e/smoke-tests/tests/modular.test.ts b/e2e/smoke-tests/tests/modular.test.ts index c6cd0b88c8e..536a271ca5e 100644 --- a/e2e/smoke-tests/tests/modular.test.ts +++ b/e2e/smoke-tests/tests/modular.test.ts @@ -86,13 +86,9 @@ import { StorageReference, deleteObject } from 'firebase/storage'; -import { - getGenerativeModel, - getAI, - AI, - VertexAIBackend -} from 'firebase/vertexai'; +import { getGenerativeModel, getAI, AI, VertexAIBackend } from 'firebase/ai'; import { getDataConnect, DataConnect } from 'firebase/data-connect'; +// @ts-ignore import { config, testAccount } from '../firebase-config'; import 'jest'; @@ -318,8 +314,8 @@ describe('MODULAR', () => { ai = getAI(app, { backend: new VertexAIBackend() }); }); it('getGenerativeModel() and countTokens()', async () => { - const model = getGenerativeModel(ai, { model: 'gemini-1.5-flash' }); - expect(model.model).toMatch(/gemini-1.5-flash$/); + const model = getGenerativeModel(ai, { model: 'gemini-2.5-flash' }); + expect(model.model).toMatch(/gemini-2.5-flash$/); const result = await model.countTokens('abcdefg'); expect(result.totalTokens).toBeTruthy; }); diff --git a/e2e/smoke-tests/tsconfig.json b/e2e/smoke-tests/tsconfig.json new file mode 100644 index 00000000000..c09f694edf7 --- /dev/null +++ b/e2e/smoke-tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "typeRoots": ["./node_modules/@types"] + } +} diff --git a/integration/compat-interop/package.json b/integration/compat-interop/package.json index f7da859c705..d578466418a 100644 --- a/integration/compat-interop/package.json +++ b/integration/compat-interop/package.json @@ -8,25 +8,25 @@ "test:debug": "karma start --browsers Chrome --auto-watch" }, "dependencies": { - "@firebase/app": "0.13.1", - "@firebase/app-compat": "0.4.1", - "@firebase/analytics": "0.10.16", - "@firebase/analytics-compat": "0.2.22", - "@firebase/auth": "1.10.7", - "@firebase/auth-compat": "0.5.27", - "@firebase/functions": "0.12.8", - "@firebase/functions-compat": "0.3.25", - "@firebase/messaging": "0.12.21", - "@firebase/messaging-compat": "0.2.21", - "@firebase/performance": "0.7.6", - "@firebase/performance-compat": "0.2.19", - "@firebase/remote-config": "0.6.4", - "@firebase/remote-config-compat": "0.2.17" + "@firebase/app": "0.14.3", + "@firebase/app-compat": "0.5.3", + "@firebase/analytics": "0.10.18", + "@firebase/analytics-compat": "0.2.24", + "@firebase/auth": "1.11.0", + "@firebase/auth-compat": "0.6.0", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20" }, "devDependencies": { "typescript": "5.5.4" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/integration/compat-interop/tsconfig.json b/integration/compat-interop/tsconfig.json index 735f3df7fbd..c986fcaf8a7 100644 --- a/integration/compat-interop/tsconfig.json +++ b/integration/compat-interop/tsconfig.json @@ -8,7 +8,7 @@ "moduleResolution": "node", "noImplicitAny": true, "outDir": "dist", - "target": "es2017", + "target": "es2020", "sourceMap": true, "esModuleInterop": true }, diff --git a/integration/compat-typings/package.json b/integration/compat-typings/package.json index 45ed087a0d7..6ad4d2e1298 100644 --- a/integration/compat-typings/package.json +++ b/integration/compat-typings/package.json @@ -13,6 +13,6 @@ "typescript": "5.5.4" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/integration/firestore/package.json b/integration/firestore/package.json index 903c182a5c0..73fc72ec219 100644 --- a/integration/firestore/package.json +++ b/integration/firestore/package.json @@ -14,8 +14,8 @@ "test:memory:debug": "yarn build:memory; karma start --auto-watch --browsers Chrome" }, "dependencies": { - "@firebase/app": "0.13.1", - "@firebase/firestore": "4.7.17" + "@firebase/app": "0.14.3", + "@firebase/firestore": "4.9.2" }, "devDependencies": { "@types/mocha": "9.1.1", @@ -26,6 +26,6 @@ "webpack-stream": "7.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/integration/messaging/package.json b/integration/messaging/package.json index 9c62c70ca5b..704d2530a62 100644 --- a/integration/messaging/package.json +++ b/integration/messaging/package.json @@ -9,7 +9,7 @@ "test:manual": "mocha --exit" }, "devDependencies": { - "firebase": "11.9.1", + "firebase": "12.3.0", "chai": "4.5.0", "chromedriver": "119.0.1", "express": "4.21.2", @@ -18,6 +18,6 @@ "selenium-assistant": "6.1.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/package.json b/package.json index 00f6bdc5f80..39455ef1161 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "homepage": "https://github.com/firebase/firebase-js-sdk", "keywords": [ @@ -111,7 +111,6 @@ "http-server": "14.1.1", "indexeddbshim": "10.1.0", "inquirer": "8.2.6", - "istanbul-instrumenter-loader": "3.0.1", "js-yaml": "4.1.0", "karma": "6.4.4", "karma-chrome-launcher": "3.2.0", @@ -143,7 +142,6 @@ "postinstall-postinstall": "2.1.0", "prettier": "2.8.8", "protractor": "5.4.2", - "protobufjs-cli": "^1.1.3", "request": "2.88.2", "semver": "7.7.1", "simple-git": "3.27.0", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 874cdb40e69..7679505f722 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,95 @@ # @firebase/ai +## 2.3.0 + +### Minor Changes + +- [`06ab5c4`](https://github.com/firebase/firebase-js-sdk/commit/06ab5c4f9b84085068381f6dff5e03b1b7cf4b2c) [#9236](https://github.com/firebase/firebase-js-sdk/pull/9236) - Added a new `InferenceMode` option for the hybrid on-device capability: `prefer_in_cloud`. When this mode is selected, the SDK will attempt to use a cloud-hosted model first. If the call to the cloud-hosted model fails with a network-related error, the SDK will fall back to the on-device model, if it's available. + +- [`9b8ab02`](https://github.com/firebase/firebase-js-sdk/commit/9b8ab02c543785226fafec056d39be7cf7ee03d1) [#9249](https://github.com/firebase/firebase-js-sdk/pull/9249) - Added Code Execution feature. + +### Patch Changes + +- [`a4848b4`](https://github.com/firebase/firebase-js-sdk/commit/a4848b401f6e8da16b0d0fdbfd064e8d68566555) [#9235](https://github.com/firebase/firebase-js-sdk/pull/9235) - Refactor component registration. + +- [`c123766`](https://github.com/firebase/firebase-js-sdk/commit/c1237662e6851936d2dd6017ab4bc7f0aa5112fd) [#9253](https://github.com/firebase/firebase-js-sdk/pull/9253) - Change documentation tags for hybrid inference from "EXPERIMENTAL" to "public preview". + +## 2.2.1 + +### Patch Changes + +- [`095c098`](https://github.com/firebase/firebase-js-sdk/commit/095c098de1e4399f3fb2993edae45060b2a8c6d0) [#9232](https://github.com/firebase/firebase-js-sdk/pull/9232) (fixes [#9231](https://github.com/firebase/firebase-js-sdk/issues/9231)) - Remove accidental `factory` export. + +## 2.2.0 + +### Minor Changes + +- [`984086b`](https://github.com/firebase/firebase-js-sdk/commit/984086b0b1bd607d3aac4cbb8400bc61416e2959) [#9224](https://github.com/firebase/firebase-js-sdk/pull/9224) - Add support for the Gemini Live API. + +- [`9b63cd6`](https://github.com/firebase/firebase-js-sdk/commit/9b63cd60efcd02b64b0d37f81affb3eabf70f9eb) [#9192](https://github.com/firebase/firebase-js-sdk/pull/9192) - Add `thoughtSummary()` convenience method to `EnhancedGenerateContentResponse`. + +- [`02280d7`](https://github.com/firebase/firebase-js-sdk/commit/02280d747863445fa1c21dfda01030412a6cecff) [#9201](https://github.com/firebase/firebase-js-sdk/pull/9201) - Add support for limited-use tokens with Firebase App Check. + These limited-use tokens are required for an upcoming optional feature called + _replay protection_. We recommend + [enabling the usage of limited-use tokens](https://firebase.google.com/docs/ai-logic/app-check) + now so that when replay protection becomes available, you can enable it sooner + because more of your users will be on versions of your app that send limited-use tokens. + +### Patch Changes + +- [`84b8bed`](https://github.com/firebase/firebase-js-sdk/commit/84b8bed35b69e4713fe8f677803cb06625525a61) [#9222](https://github.com/firebase/firebase-js-sdk/pull/9222) - Fixed an issue where `AIError` messages were too long after including an entire response body. + +- [`c5f08a9`](https://github.com/firebase/firebase-js-sdk/commit/c5f08a9bc5da0d2b0207802c972d53724ccef055) [#9216](https://github.com/firebase/firebase-js-sdk/pull/9216) - Add 'includeSafetyAttributes' field to Predict request payloads. + +- [`cbef6c6`](https://github.com/firebase/firebase-js-sdk/commit/cbef6c6e5b752c316104f9c834e0fe21b75c3ef1) [#9225](https://github.com/firebase/firebase-js-sdk/pull/9225) - Exclude ChromeAdapterImpl code from Node entry point. + +## 2.1.0 + +### Minor Changes + +- [`e25317f`](https://github.com/firebase/firebase-js-sdk/commit/e25317f9f3c58305bc093e4f2e676690feb16db0) [#9029](https://github.com/firebase/firebase-js-sdk/pull/9029) - Add hybrid inference options to the Firebase AI SDK. + +## 2.0.0 + +### Major Changes + +- [`5200f7b`](https://github.com/firebase/firebase-js-sdk/commit/5200f7bb777cf2260dcd396fbd19ac6cc7cb44c4) [#9042](https://github.com/firebase/firebase-js-sdk/pull/9042) - Add support for `anyOf` schemas + +- [`e59cd7d`](https://github.com/firebase/firebase-js-sdk/commit/e59cd7da1f375ec89f237ceb684c9f450d65cd34) [#9137](https://github.com/firebase/firebase-js-sdk/pull/9137) - Convert TS enums exports in Firebase AI into const variables. + +- [`cb19688`](https://github.com/firebase/firebase-js-sdk/commit/cb19688bf3d339a46c4964cb30b6263af08526e6) [#9079](https://github.com/firebase/firebase-js-sdk/pull/9079) - Remove GroundingAttribution + +- [`ec5f374`](https://github.com/firebase/firebase-js-sdk/commit/ec5f37403d9ebe28d3d71a7789d59edfb12762df) [#9063](https://github.com/firebase/firebase-js-sdk/pull/9063) - Remove `VertexAI` APIs. + +### Minor Changes + +- [`a4ccd25`](https://github.com/firebase/firebase-js-sdk/commit/a4ccd254dd1ecb63aa010ca010ad50d4b8a8316a) [#9068](https://github.com/firebase/firebase-js-sdk/pull/9068) - Add support for Grounding with Google Search. + +- [`6ab4e13`](https://github.com/firebase/firebase-js-sdk/commit/6ab4e13a1665dab4be89ecc141b4584a5a6df569) [#9156](https://github.com/firebase/firebase-js-sdk/pull/9156) - Add support for Thinking Budget. + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`ae976d0`](https://github.com/firebase/firebase-js-sdk/commit/ae976d02908a5a8913c5fcd4c0485fcf4b081fec) [#8948](https://github.com/firebase/firebase-js-sdk/pull/8948) (fixes [#8944](https://github.com/firebase/firebase-js-sdk/issues/8944)) - Fix typings for `functionDeclaration.parameters`. + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 1.4.1 + +### Patch Changes + +- [`b97eab3`](https://github.com/firebase/firebase-js-sdk/commit/b97eab36a3553c906c35f4751a0b17c717178b13) [#9090](https://github.com/firebase/firebase-js-sdk/pull/9090) - Add deprecation label to `totalBillableCharacters`. `totalTokens` should be used instead. + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 1.4.0 ### Minor Changes diff --git a/packages/ai/integration/constants.ts b/packages/ai/integration/constants.ts index 1adfa4f47a0..f4a74e75039 100644 --- a/packages/ai/integration/constants.ts +++ b/packages/ai/integration/constants.ts @@ -54,6 +54,12 @@ const backendNames: Map = new Map([ const modelNames: readonly string[] = ['gemini-2.0-flash', 'gemini-2.5-flash']; +// The Live API requires a different set of models, and they're different for each backend. +const liveModelNames: Map = new Map([ + [BackendType.GOOGLE_AI, ['gemini-live-2.5-flash-preview']], + [BackendType.VERTEX_AI, ['gemini-2.0-flash-exp']] +]); + /** * Array of test configurations that is iterated over to get full coverage * of backends and models. Contains all combinations of backends and models. @@ -69,6 +75,25 @@ export const testConfigs: readonly TestConfig[] = backends.flatMap(backend => { }); }); +/** + * Test configurations used for the Live API integration tests. + */ +export const liveTestConfigs: readonly TestConfig[] = backends.flatMap( + backend => { + const testConfigs: TestConfig[] = []; + liveModelNames.get(backend.backendType)!.forEach(modelName => { + const ai = getAI(app, { backend }); + testConfigs.push({ + ai, + model: modelName, + toString: () => formatConfigAsString({ ai, model: modelName }) + }); + }); + + return testConfigs; + } +); + export const TINY_IMG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII='; export const IMAGE_MIME_TYPE = 'image/png'; diff --git a/packages/ai/integration/generate-content.test.ts b/packages/ai/integration/generate-content.test.ts index 22e4b0a30ac..ffb1ecca698 100644 --- a/packages/ai/integration/generate-content.test.ts +++ b/packages/ai/integration/generate-content.test.ts @@ -17,17 +17,22 @@ import { expect } from 'chai'; import { + BackendType, Content, GenerationConfig, HarmBlockThreshold, HarmCategory, + Language, Modality, + Outcome, SafetySetting, + URLRetrievalStatus, getGenerativeModel } from '../src'; import { testConfigs, TOKEN_COUNT_DELTA } from './constants'; -describe('Generate Content', () => { +describe('Generate Content', function () { + this.timeout(20_000); testConfigs.forEach(testConfig => { describe(`${testConfig.toString()}`, () => { const commonGenerationConfig: GenerationConfig = { @@ -39,19 +44,19 @@ describe('Generate Content', () => { const commonSafetySettings: SafetySetting[] = [ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, - threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + threshold: HarmBlockThreshold.BLOCK_NONE }, { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, - threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + threshold: HarmBlockThreshold.BLOCK_NONE }, { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, - threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + threshold: HarmBlockThreshold.BLOCK_NONE }, { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, - threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + threshold: HarmBlockThreshold.BLOCK_NONE } ]; @@ -91,6 +96,10 @@ describe('Generate Content', () => { 2, TOKEN_COUNT_DELTA ); + expect(response.usageMetadata!.thoughtsTokenCount).to.be.closeTo( + 30, + TOKEN_COUNT_DELTA * 2 + ); expect(response.usageMetadata!.totalTokenCount).to.be.closeTo( 55, TOKEN_COUNT_DELTA * 2 @@ -144,6 +153,198 @@ describe('Generate Content', () => { } }); + it('generateContent: google search grounding', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ googleSearch: {} }] + }); + + const result = await model.generateContent( + 'What is the speed of light in a vaccuum in meters per second?' + ); + const response = result.response; + const trimmedText = response.text().trim(); + const groundingMetadata = response.candidates?.[0].groundingMetadata; + expect(trimmedText).to.contain('299,792,458'); + expect(groundingMetadata).to.exist; + expect(groundingMetadata!.searchEntryPoint?.renderedContent).to.contain( + 'div' + ); + expect( + groundingMetadata!.groundingChunks + ).to.have.length.greaterThanOrEqual(1); + groundingMetadata!.groundingChunks!.forEach(groundingChunk => { + expect(groundingChunk.web).to.exist; + expect(groundingChunk.web!.uri).to.exist; + }); + expect( + groundingMetadata?.groundingSupports + ).to.have.length.greaterThanOrEqual(1); + groundingMetadata!.groundingSupports!.forEach(groundingSupport => { + expect( + groundingSupport.groundingChunkIndices + ).to.have.length.greaterThanOrEqual(1); + expect(groundingSupport.segment).to.exist; + expect(groundingSupport.segment?.endIndex).to.exist; + expect(groundingSupport.segment?.text).to.exist; + // Since partIndex and startIndex are commonly 0, they may be omitted from responses. + }); + }); + + describe('URL Context', async () => { + // URL Context is not supported in Google AI for gemini-2.0-flash + if ( + testConfig.ai.backend.backendType === BackendType.GOOGLE_AI && + testConfig.model === 'gemini-2.0-flash' + ) { + return; + } + + it('generateContent: url context', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ urlContext: {} }] + }); + + const result = await model.generateContent( + 'Summarize this website https://berkshirehathaway.com' + ); + const response = result.response; + const urlContextMetadata = + response.candidates?.[0].urlContextMetadata; + expect(urlContextMetadata?.urlMetadata).to.exist; + expect( + urlContextMetadata?.urlMetadata.length + ).to.be.greaterThanOrEqual(1); + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.exist; + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.equal( + 'https://berkshirehathaway.com' + ); + expect( + urlContextMetadata?.urlMetadata[0].urlRetrievalStatus + ).to.equal(URLRetrievalStatus.URL_RETRIEVAL_STATUS_SUCCESS); + + const usageMetadata = response.usageMetadata; + expect(usageMetadata).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.be.greaterThan(0); + }); + + it('generateContent: url context and google search grounding', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ urlContext: {} }, { googleSearch: {} }] + }); + + const result = await model.generateContent( + 'According to https://info.cern.ch/hypertext/WWW/TheProject.html, what is the WorldWideWeb? Search the web for other definitions.' + ); + const response = result.response; + const trimmedText = response.text().trim(); + const urlContextMetadata = + response.candidates?.[0].urlContextMetadata; + const groundingMetadata = response.candidates?.[0].groundingMetadata; + expect(trimmedText).to.contain( + 'hypermedia information retrieval initiative' + ); + expect(urlContextMetadata?.urlMetadata).to.exist; + expect( + urlContextMetadata?.urlMetadata.length + ).to.be.greaterThanOrEqual(1); + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.exist; + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.equal( + 'https://info.cern.ch/hypertext/WWW/TheProject.html' + ); + expect( + urlContextMetadata?.urlMetadata[0].urlRetrievalStatus + ).to.equal(URLRetrievalStatus.URL_RETRIEVAL_STATUS_SUCCESS); + expect(groundingMetadata).to.exist; + expect(groundingMetadata?.groundingChunks).to.exist; + expect( + groundingMetadata?.groundingChunks!.length + ).to.be.greaterThanOrEqual(1); + expect( + groundingMetadata?.groundingSupports!.length + ).to.be.greaterThanOrEqual(1); + + const usageMetadata = response.usageMetadata; + expect(usageMetadata).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.be.greaterThan(0); + }); + + it('generateContent: url context and google search grounding without URLs in prompt', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ urlContext: {} }, { googleSearch: {} }] + }); + + const result = await model.generateContent( + 'Recommend 3 books for beginners to read to learn more about the latest advancements in Quantum Computing.' + ); + const response = result.response; + const urlContextMetadata = + response.candidates?.[0].urlContextMetadata; + const groundingMetadata = response.candidates?.[0].groundingMetadata; + if (testConfig.ai.backend.backendType === BackendType.GOOGLE_AI) { + expect(urlContextMetadata?.urlMetadata).to.exist; + expect( + urlContextMetadata?.urlMetadata.length + ).to.be.greaterThanOrEqual(1); + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.exist; + expect( + urlContextMetadata?.urlMetadata[0].urlRetrievalStatus + ).to.equal(URLRetrievalStatus.URL_RETRIEVAL_STATUS_SUCCESS); + expect(groundingMetadata).to.exist; + expect(groundingMetadata?.groundingChunks).to.exist; + + const usageMetadata = response.usageMetadata; + expect(usageMetadata).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.be.greaterThan(0); + } else { + // URL Context does not integrate with Google Search Grounding in Vertex AI + expect(urlContextMetadata?.urlMetadata).to.not.exist; + expect(groundingMetadata).to.exist; + expect(groundingMetadata?.groundingChunks).to.exist; + } + }); + }); + + it('generateContent: code execution', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ codeExecution: {} }] + }); + const prompt = + 'What is the sum of the first 50 prime numbers? ' + + 'Generate and run code for the calculation, and make sure you get all 50.'; + + const result = await model.generateContent(prompt); + const parts = result.response.candidates?.[0].content.parts; + expect( + parts?.some(part => part.executableCode?.language === Language.PYTHON) + ).to.be.true; + expect( + parts?.some(part => part.codeExecutionResult?.outcome === Outcome.OK) + ).to.be.true; + // Expect these to be truthy (!= null) + expect(parts?.some(part => part.executableCode?.code != null)).to.be + .true; + expect(parts?.some(part => part.codeExecutionResult?.output != null)).to + .be.true; + }); + it('generateContentStream: text input, text output', async () => { const model = getGenerativeModel(testConfig.ai, { model: testConfig.model, diff --git a/packages/ai/integration/live.test.ts b/packages/ai/integration/live.test.ts new file mode 100644 index 00000000000..caa18970ab7 --- /dev/null +++ b/packages/ai/integration/live.test.ts @@ -0,0 +1,327 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { + BackendType, + getLiveGenerativeModel, + LiveGenerationConfig, + LiveServerContent, + LiveServerToolCall, + LiveServerToolCallCancellation, + ResponseModality +} from '../src'; +import { liveTestConfigs } from './constants'; +import { HELLO_AUDIO_PCM_BASE64 } from './sample-data/hello-audio'; + +// A helper function to consume the generator and collect text parts from one turn. +async function nextTurnText( + stream: AsyncGenerator< + LiveServerContent | LiveServerToolCall | LiveServerToolCallCancellation + > +): Promise { + let text = ''; + // We don't use `for await...of` on the generator, because that would automatically close the generator. + // We want to keep the generator open so that we can pass it to this function again to get the + // next turn's text. + let result = await stream.next(); + while (!result.done) { + const chunk = result.value as + | LiveServerContent + | LiveServerToolCall + | LiveServerToolCallCancellation; + switch (chunk.type) { + case 'serverContent': + if (chunk.turnComplete) { + return text; + } + + const parts = chunk.modelTurn?.parts; + if (parts) { + parts.forEach(part => { + if (part.text) { + text += part.text; + } else { + throw Error(`Expected TextPart but got ${JSON.stringify(part)}`); + } + }); + } + break; + default: + throw new Error(`Unexpected chunk type '${(chunk as any).type}'`); + } + + result = await stream.next(); + } + + return text; +} + +describe('Live', function () { + this.timeout(20000); + + const textLiveGenerationConfig: LiveGenerationConfig = { + responseModalities: [ResponseModality.TEXT], + temperature: 0, + topP: 0 + }; + + liveTestConfigs.forEach(testConfig => { + if (testConfig.ai.backend.backendType === BackendType.VERTEX_AI) { + return; + } + describe(`${testConfig.toString()}`, () => { + describe('Live', () => { + it('should connect, send a message, receive a response, and close', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + await session.send( + 'Where is Google headquarters located? Answer with the city name only.' + ); + const responseText = await responsePromise; + expect(responseText).to.exist; + expect(responseText).to.include('Mountain View'); + await session.close(); + }); + it('should handle multiple messages in a session', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const generator = session.receive(); + + await session.send( + 'Where is Google headquarters located? Answer with the city name only.' + ); + + const responsePromise1 = nextTurnText(generator); + const responseText1 = await responsePromise1; // Wait for the turn to complete + expect(responseText1).to.include('Mountain View'); + + await session.send( + 'What state is that in? Answer with the state name only.' + ); + + const responsePromise2 = nextTurnText(generator); + const responseText2 = await responsePromise2; // Wait for the second turn to complete + expect(responseText2).to.include('California'); + + await session.close(); + }); + + it('close() should be idempotent and terminate the stream', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model + }); + const session = await model.connect(); + const generator = session.receive(); + + // Start consuming but don't wait for it to finish yet + const consumptionPromise = (async () => { + // This loop should terminate cleanly when close() is called + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _ of generator) { + } + })(); + + await session.close(); + + // Calling it again should not throw an error + await session.close(); + + // Should resolve without timing out + await consumptionPromise; + }); + }); + + describe('sendMediaChunks()', () => { + it('should send a single audio chunk and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + await session.sendMediaChunks([ + { + data: HELLO_AUDIO_PCM_BASE64, // "Hey, can you hear me?" + mimeType: 'audio/pcm' + } + ]); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + + it('should send multiple audio chunks in a single batch call', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + // TODO (dlarocque): Pass two PCM files with different audio, and validate that the model + // heard both. + await session.sendMediaChunks([ + { data: HELLO_AUDIO_PCM_BASE64, mimeType: 'audio/pcm' }, + { data: HELLO_AUDIO_PCM_BASE64, mimeType: 'audio/pcm' } + ]); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + + describe('sendMediaStream()', () => { + it('should consume a stream with multiple chunks and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + // TODO (dlarocque): Pass two PCM files with different audio, and validate that the model + // heard both. + const testStream = new ReadableStream({ + start(controller) { + controller.enqueue({ + data: HELLO_AUDIO_PCM_BASE64, + mimeType: 'audio/pcm' + }); + controller.enqueue({ + data: HELLO_AUDIO_PCM_BASE64, + mimeType: 'audio/pcm' + }); + controller.close(); + } + }); + + await session.sendMediaStream(testStream); + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + + /** + * These tests are currently very unreliable. Their behavior seems to change frequently. + * Skipping them for now. + */ + /* + describe('function calling', () => { + // When this tests runs against the Google AI backend, the first message we get back + // has an `executableCode` part, and then + it('should trigger a function call', async () => { + const tool: FunctionDeclarationsTool = { + functionDeclarations: [ + { + name: 'fetchWeather', + description: + 'Get the weather conditions for a specific city on a specific date.', + parameters: Schema.object({ + properties: { + location: Schema.string({ + description: 'The city of the location' + }), + date: Schema.string({ + description: 'The date to fetch weather for.' + }) + } + }) + } + ] + }; + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + tools: [tool], + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const generator = session.receive(); + + const streamPromise = new Promise(async resolve => { + let text = ''; + let turnNum = 0; + for await (const chunk of generator) { + console.log('chunk', JSON.stringify(chunk)) + switch (chunk.type) { + case 'serverContent': + if (chunk.turnComplete) { + // Vertex AI only: + // For some unknown reason, the model's first turn will not be a toolCall, but + // will instead be an executableCode part in Google AI, and a groundingMetadata in Vertex AI. + // Let's skip this unexpected first message, waiting until the second turn to resolve with the text. This will definitely break if/when + // that bug is fixed. + if (turnNum === 0) { + turnNum = 1; + } else { + return resolve(text); + } + } else { + const parts = chunk.modelTurn?.parts; + if (parts) { + text += parts.flatMap(part => part.text).join(''); + } + } + break; + case 'toolCall': + // Send a fake function response + const functionResponse: FunctionResponsePart = { + functionResponse: { + id: chunk.functionCalls[0].id, // Only defined in Google AI + name: chunk.functionCalls[0].name, + response: { degrees: '22' } + } + }; + console.log('sending', JSON.stringify(functionResponse)) + await session.send([functionResponse]); + break; + case 'toolCallCancellation': + throw Error('Unexpected tool call cancellation'); + default: + throw Error('Unexpected chunk type'); + } + } + }); + + // Send a message that should trigger a function call to fetchWeather + await session.send('Whats the weather on June 15, 2025 in Toronto?'); + + const finalResponseText = await streamPromise; + expect(finalResponseText).to.include('22'); // Should include the result of our function call + + await session.close(); + }); + }); + */ + }); + }); +}); diff --git a/packages/ai/integration/sample-data/hello-audio.ts b/packages/ai/integration/sample-data/hello-audio.ts new file mode 100644 index 00000000000..7c3b8f2f693 --- /dev/null +++ b/packages/ai/integration/sample-data/hello-audio.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const HELLO_AUDIO_PCM_BASE64 = + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAP//AAAAAAAAAQABAAEAAQAAAAAAAAD///7//v8AAAIAAgAAAP7/AgAKAAcA7P+1/3r/Xf9x/67/8v8dACgAIAAZABgAGwAZABIADQAKAAkACAAGAAYACAALAAsACQAJAAsADwATABUAFQATABIAEAARABIAFAAVABMAEAANAAsACwALAAoACgAKAAoACgAKAAoACQAJAAkACAAIAAgACAAIAAgACAAHAAcABwAHAAYABgAGAAYABQAFAAUABQAFAAUABAAEAAQAAwADAAMAAwADAAMAAgACAAIAAgABAAEAAQAAAAAAAAAAAAAAAAAAAAAA/////////////////v/+//7//v/+//7//v/+//7//v/+//7//f/9//3//f/9//3//f/9//3//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z/+//7//v//P/8//v/+//7//v/+//7//v/+//7//v/+//8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//3//f/9//3//f/9//z//P/8//z//P/8//3//f/9//3//f/9//3//f/9//3//f/9//3//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v////////////////////7//v//////////////////////AAAAAAAA///9//3/AAADAAQAAQD9//r//P8BAAUABAAAAP3//P///wEAAQAAAP//AAACAAIAAgABAAEAAQABAAEAAQAAAAAAAQABAAEAAQABAAEAAQABAAEAAgACAAIAAgACAAIAAgABAAEAAQACAAIAAgACAAIAAgACAAEAAQABAAEAAQABAAEAAgACAAIAAgACAAIAAgACAAIAAgACAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgABAAEAAQABAAEAAQABAAEAAQACAAIAAgACAAIAAgABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////8AAP///////////////////////////////wAAAAD///////////////////////////////////////////////////////////////////////////////////////8AAAAAAAAAAAAAAAAAAAAA//////////////////8AAAAAAAD///////////////////////////////////////////7//v//////////////////////////////////////AAAAAAAAAQABAAEAAQABAAEAAQABAAIAAgACAAIAAgABAAEAAQABAAAAAAAAAAAAAAD//wAA//////7//v/+//7//f/9//z//f/9//3//f/9//z//P/7//z//P/8//z//P/8//z/+//6//r/+v/6//r/+v/6//n/+f/5//n/+f/5//n/+f/5//n/+P/3//f/9//3//j/+P/3//f/9v/2//f/+f/5//n/+v/7//v//f/9////AQABAAIABAADAAMABQAHAAgACQALAAwADAAMAAsACgAKAAkACwAMAAsACgAJAAkABwAFAAUABgAHAAUABQAFAAQABAACAAQABQACAAEAAQACAAIAAAABAAMAAgACAAMABgAEAAIAAwAEAAIAAQADAAYABQAEAAUABgAIAAYAAwAJAAwACgAJAA4AEAARABAADwAUABgAFQAUABwAHAAVABQAFwAWABEADwASABEACwAHAAYABQAGAAcACgAPABAADgANAA4ADgARABIAFgAaABsAGQAVABEAEwAXABgAFAATABcAEQAIAAoADwAMAAYAAgAAAAAA/v/7//v/+P/4//7/AAD3//D/8f/0//D/7v/1//r/9//z//X/+P/z/+v/7f/v/+z/6f/q/+3/7v/u/+r/5f/l/+f/6v/w//D/6P/p/+r/5f/m/+z/7f/p/+f/5P/f/+b/7f/w//L/7P/m/+7/9f/3//7/BAABAPr/+P/8/wEAAgADAAYABAD9//v///8GAAkAAwAAAAAA+P/x//b/9//0//T/+/8AAPr/9//+/wIA/f/2//r/AQD+//3/CwAYABMADwANAAoAEQAZABAACQALAA4AEQANAAEA/v8FAAMAAQAFAAAA9//u/+b/5//p/+L/4v/n/9v/zv/O/8//zf/O/9X/4f/m/9v/1//b/9//5P/m/+b/1//R/9T/3v/m/+P/2//O/8j/xf/B/9H/4//h/+v/8f/y//T/+/8MAAoABgAFAP7/EQAWAA8AJQAzACoANAA+AC0AHQATABsAKQAjABYAHQAWAAsAFwAkADUAPQAzACsAHwAMAAkAIAArACYAGgAIAAkADQAPABsALwA0ACsAKgArABwAGQAdABgAFAAHAO7/7f/v/+v/6f/3/+r/yf/I/8//yP/G/77/uP/H/8b/0f/Z/9T/1f/L/8D/zf/Q/83/2P/b/9j/2//i/9n/4v/u/97/4P/k/9j/9f/7/9v/6v/6//H/CQAnACoAEQAHABgAJAApACkAKAA4AEUAOgA1ACgAFwAQABoAAQDl/93/0f/C/8P/uf+1/6n/lf+V/7D/s/+2/+L/6f/U//P/+P/n//j/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UwBdAFQATAA/ADwALwAcAAkACgAAAOz/8v/6//X/7//v//b/5//T/83/yf+//7r/t/+o/7r/v/+2/7f/xf/A/7T/sv+9/8n/yP/C/8r/uv/B/8b/wv+6/7//wf+x/7P/uf+5/8D/vv+4/8H/u//Q/7r/vv/E/8b/y//X/+7/DQAmACoAVgBoAGsAeAByAHUAdQBoAHUAhgCEAIcAjQCRAIgAkACEAIYAfwB1AGUAVgBJAEkARAA9ACwAMAAuAC8AMwBDADsAQgBCADkAOgA5ADcANgBCAEcAQwBCAD4ARAAzACUAJAA2ACsAQAAyACYAJAAJAP3/DAAJAAkACQD6/+r/9P/1/+z/7v/o/+r/+v/2/wQAAQAQABoAIwAqACsAKwAxACoAJQAgABEADgAHAPz//P8IAA8AEQD7//b/6f/f/+P/1//a/8X/yf+3/8f/wf+9/7f/sf+5/7D/vP+u/7b/q/+r/7f/sf+m/53/rP+d/53/of+v/67/qv/A/8P/0//r////DgAMAB0AKAA3ADAAOgA1ADsAOwA1AB4AJwAmAB8AGwAJAAAA///o/+//8f8FAA4ADgATAPr/7//6//X/9P/m/+f/5//f/+f/1v/Y/9//4//l/+//9P/4//f/6//6//b/9//0//f/7f/l/9j/2P/e/8P/vf+3/7j/tP+8/7T/xv+x/67/o/+k/6n/sP+v/7X/rP+2/73/xP/B/77/yv/N/9P/2P/3//r//f8BAP//CwAVABcAIQApACwANgA0AD8ARQBAAEIARwBBADUAOgBBADsAPABKAFAASQBKAFQAVABUAGIAZgBeAFwAXABLAE8ARwBTAEwAOAAsADIALQAoADgAKQAuADEAKgA2ACsAMAArACMAKgApACcAMwAkAB4ADgASAAYA+/8BAAAAGQAHAAsACwADAPz/CwD9//f/7//Y/9X/1v/T/8j/0P/W/8v/5v/h//T/AAAIABcAEwAMABMAFwD8/woA/v/9//z/AAD9/+P/AADy/+7/+f////7/+v8MAAUABwANABQAGwAcADEAKQA5AEwATgBPAE0AXABNAFwAaABcAGAAVgBhAF0AUwBIADwAQQAvACsAKgAoACEAIgAhABgAHQATACUAHwAdAB4AGAAbABAACwAKAAIABQD0//7/+//+//b/+P/x/9j/3//U/93/zP/N/8z/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iv+N/5H/nP+j/6z/sf+4/77/xP/O/9n/6v/7/wgAEAAWACEAKgAuADIANwA2ADoAOwA4ACsAGwAWABEADwAKAAQAAQD3/+r/5f/g/+T/4//d/9z/1P/Y/9b/0//Q/83/x//F/8n/yP/H/8H/wf/F/8n/1P/C/9P/6//9/w0ACQATABMAGwAmACQAJAAmACwALwAqAC0AMAAzADEAJAAgABoAFwAVAA4ACwABAPz/9v/y//P/6f/o/+n/7P/z//3/CAAHAAQAAwD9//j/+v/4/+z/2v/S/9r/5//q/+H/1P/U/+H/7/8AAAAA/v/9/wYAIQA6AEcAPQAyADoAVQB2AIsAiQB+AIYAlgClAKUAnQCVAIkAewBwAGYAXgBWAEwATwBQAFAAUwBXAFIASgBIAEkASABFADQAIgAkAC8ALAAhABAACQAPABIADAAAAPv/+v/3//T/8P/u/+7/8v/8/wcAEwAeACMAIQAhACMAJwAiABwADQD///z/+//6//v/9v/5/wIADAARABIAFQAYAB8AKgAxADYAOgA9AD4AQABEAE0AWQBdAFcAVQBbAGYAawBlAGMAZABoAGsAYwBbAF0AXgBhAGIAWQBHAD0AMwApACEAHQAeACEAKwAyADMAKgAYAA4ABQD5//L/7P/h/83/xv/Y/+//8//Z/83/2v/l/+b/2f/I/8X/zf/F/6//ov+a/5j/k/+J/4L/hP+J/5L/nP+j/67/tv+1/7n/vP+3/7L/r/+1/7j/s/+v/6n/pP+p/7P/tv+5/8T/x//D/8L/yf/S/9X/1P/R/9n/4P/h/+T/5//w//b/8//q/+n/7P/t/+3/6//w//H/8v/0//P/7//o/+v/7//x//L/9P///wwAFwAaABkAGgAcABwAFAAGAPz//P////n/8P/w//f/+//+////AAABAAEAAgAIAA0ADgAPABEADgAQABIAHAAkACgAKwAnABwAEwAPAAkA/v/x/+n/6v/u//L/9P/1//L/8f/t/+b/4//g/+H/6P/u/+7/8P/4//7/AgAIAA8AGQAjACwAKgAlACEAHwAeABgAEgATABMAEgAPAA4ADQAKAAoACwANAA8AEwATABMAGQAaABgAGgAgACMAIAAnAC4ALwAyADEANgA7ADkAMAAlACAAIgAoACgAJgAfABUADAADAAAAAgACAAIA/P/6//j/8f/q/+T/4//Y/8z/xv++/7b/qf+g/5r/kf+J/4P/gf+E/4f/i/+R/57/pP+n/6z/sf+3/7//x//K/9D/0v/U/9f/2f/X/9P/z//K/8n/zP/V/9r/3//h/9v/2P/X/9X/1//d/+P/4//i/+P/4v/g/9z/1P/T/9L/1v/c/+P/7f/4//z//f8DAAIAAwAGAAMABwAMAAoADAAMAAwADAABAPn/+//+/wAA/v/5//f/+/8AAAEA/////wIABgAQABoAIQArADAAMwA3ADwARABMAEkAPwA5ADYAOwBCAEIAQABDAEUASgBMAEwAUABUAFUAVQBWAFkAYABfAFUATwBXAGQAaQBfAFQAWwBnAF8ARwA3AD8AUQBRADgAIAAcACMAGwAGAPv/+f/2/+7/4v/Z/9P/z//L/8r/zf/V/9f/0v/K/8b/zP/W/9T/zP/M/8f/vv+6/7v/u/+z/6z/qv+z/7z/vf+8/8X/1P/o//L/8P/w//b///8EABMAJAArACcAIwApADMAOAAzADEANwA4ADIAKgAkACQAIAAbAB4AHQAdABwAFQAOAA8ADAAMAAoAAAD2//T/9//5//f/9//9/wIACAAQABYAGgAfACwAOQA/AEYATQBNAFAAVgBdAGQAZwBpAGoAagBrAHEAcQBlAFYASQBAADkAMAAtACcAKAApACIAHgAgACkAKgAnACMAHwAZABEADAACAPv/8//m/9v/0f/J/8b/wP+1/6z/qP+g/5X/kP+Q/5P/j/+L/43/lP+c/6L/qP+x/77/wv/G/8z/0v/W/9L/zP/M/8v/zP/K/8b/wf/A/8L/vf+5/7j/uf+6/7z/v/++/77/u/+3/7b/tf+1/7n/vf/C/8P/yf/S/9b/0v/G/8D/wv/C/8L/x//M/9P/2f/Z/9v/5P/z/wQAFAAnADoATQBiAGYAYwBhAFgAVABVAFQAUQBPAE4ATgBIAEoATABNAE0ARwBGAEoASwBJAD8ANQA5AD0ANgAuACgAJAAgABwAGQAaAB0AIAAfABgAFgATABUAHQAjAC8ANgAzAC0AKgAbABYADwAJAA0ACQAHAAUACwAQAA0AEgARAA8ACgACAAQACAALABAADwACAPT/3//S/8f/wf/F/8n/zf/H/73/t/+v/6T/l/+T/5f/pf+v/67/sP+t/6z/rv+0/7r/wv/H/9D/3f/n//D/9v/2//P/8P/3//7/BgASACEALQAsACsAJgAmAC0AMwA+AFAAXgBrAHUAcgB0AHEAYQBaAE8ARgBGAEcAQwA5ADAAJgAdABgAEgAJAP7//f/4/+v/6v/l/9//5f/m/9r/4P/g/9X/1v/R/8b/xP+7/7H/rP+u/7f/w//L/83/0P/Q/8//z//V/+T/6//x//f/AgALAAsABwAHAAoADQASABgAFgAWABoAGwAaABAACwAMAAkACQAYACUAKgArAC8ALAAoACsALgAxADAAKgAmACAAHQAXAA8ADQAIAA0AHAAdABwAFgAQAAgA/v/1/+P/4f/c/9b/2P/V/9X/2P/W/8f/xf/M/8f/wv+4/7X/sv+t/63/pf+a/5f/nv+s/7v/xf/O/9f/2P/a/9//4f/f/+D/7P/1//j/8P/i/9j/0P/I/8H/wf/F/87/1P/V/9v/5f/t//L/8//2//3/AwALAA4AEQAZABsAHgAiACEAJgArAC8AMAAyADYANgA6AEAAQQA8ADkANwA3ADkAMwAtACsAGwAIAPX/6P/k/9r/1f/P/8P/vf+0/63/rv+z/7T/tf+z/63/qf+r/7D/sP+x/7j/wf/G/8f/zf/U/93/6//7/wYADQAWACAALAA2ADgAQgBOAE8AVQBZAF4AZQBjAFgASQBFAEgASwBLAEgARgBIAEUAQQBAADsAOAA+AD4ANAAwACkAJAAjABoAEwANAAQA/v/6//L/6//u/+3/5P/c/9H/zf/L/8v/0P/N/9H/0P/O/9X/zv/O/9P/1f/W/9D/zv/R/9j/3f/k/+r/6//t/+3/6//u//L/9f/2//n//f8EAA8AFwAeACAAJQAlACYALwAwACoAIgAhACQAIwAhAB8AJwAmACcAJwAgACcALwAyADMANgA6ADgAMQAkACEAJgAmACcALQA0ADcANQAvACUAKQAuAC4AKgAlACMAIAAeABoADgAJAAgACwAQABAAFgAWABYAGAAWABYAEgATABUAGQAZABgAGgAkACoAHwAeABsAHQAmACYAJgApADEAMgAwACoAJQAnACgAJAAoACkAIgAmACsAKwA1ADkAQABKAE4AUQBUAFYAVQBZAF4AYgBfAFwAUQBFADoAMQAmAB4AHAAWABEADQALAAkACAAKABEAEAANAAoA///0/+//5f/g/+P/4f/i/+H/3f/e/+X/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vv+9/73/t/+w/6b/of+n/7D/t/++/8b/zv/U/9r/3v/e/+D/3v/d/93/2//d/+L/4v/j/+L/3//Z/9X/0v/U/9j/1f/U/9P/0v/V/9b/1//X/9z/4//p/+n/6P/q/+//9f/7/wIABwANABQAHwAjAB8AFQAQABQAGwAgACMAJwAuAC4ALAAoACcAJwAsADMANwA3ADAALAApACMAHgAcABYAEAAQABUAHQAfACEAJgAoACgAJwAmACQAKAApACgAJgAkAB8AGwAbABoAGQAQAAwADQANAA8AEQARABIAFAASABEAFwAfACUAIwAjACUAJQAmACQAJwApACoAKwAtADUAOgA+AD8ARgBNAFAASwBFAEMAPgA4ADEAMAAtACcAHAAXABYAFQATABAADwAMAAoACgANAA0ADwAHAAIA/f/7//v/9v/z/+//7//u/+z/6f/x//r/AAD///j/9f/5//3//f8AAAYACAAIAAYABQAEAP///v/+//7/+f/w/+n/5v/k/93/2f/V/9L/0v/R/9P/0P/O/83/y//N/8v/y//L/9H/1//Z/9n/2P/a/9v/3v/b/9v/3v/g/+f/9P8AAAsAGAAjACoALQAqACgAJQAiAB8AIwAjACEAKAAxADYAPwBCAEQARgBLAE8AVQBeAGYAaABmAF4AVABIAD0AMQAnACQAHQAQAAEA9f/v/+b/1f/E/7//wf/F/8X/wv/D/8f/y//Q/9D/1P/b/93/3f/Z/9n/1f/R/9H/0f/N/8r/vv+4/7H/rP+k/53/nv+d/6L/pf+p/6z/q/+u/7H/sv+4/7//w//I/83/3P/p//D/+v/+/wIABQAEAAAA+f/2//X/8f/y//X/8f/u/+7/8P/u//H/9//4//n/9v/3//b/+P///wMA/v/3/+3/4//f/93/3//i/+P/4v/l/+f/6P/p/+f/5v/k/+H/3f/e/+D/6P/u//P/+v///wIAAwAHAAkACwAHAAUABgABAPz/+f/2//n/+//8/wMACgAUAB4AIwAkACYAKQApACYAIAAgAB4AHwAdABgADgAHAP///P/8//f/8f/w//D/8v/2/wIAEgAZAB0AIQAsADMAOAA0ADIAKgAfABMABAD+//b/7//k/9r/0f/I/8T/xP/H/83/zv/O/9H/2f/f/+L/6P/s//L/9P/z//T/8//t/+f/4//f/+D/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KgArACIAGgAZABUAEQAMAAoABAD5/+//7f/u/+X/2v/O/8H/s/+p/6f/p/+n/6n/sf+t/6D/nP+Y/5r/p/+r/6//v//I/87/zf/N/9X/3//f/9b/0//P/8v/z//T/9b/2f/Y/9n/3P/Z/93/4P/c/9v/3f/c/+L/5P/k/+P/3//c/+D/4f/g/+b/8v/2//n/+f/5//X/7P/n/+b/3f/Z/9z/2f/a/97/3//c/9X/zf/C/7T/p/+Y/5n/nf+Z/4v/f/99/3n/bf9o/2X/Zv9o/2z/bv9u/27/cP90/3T/eP91/3f/gP+G/4n/jP+O/47/lP+b/6T/pv+h/53/nv+e/6X/q/+s/7P/u//C/83/1f/U/9f/4//p//H/+P8BAAgABwAFAAgACQAKAAsACQAIAAEA8v/s/+f/4v/b/9X/0f/Q/9L/1P/U/9f/3P/b/9T/z//L/9H/2//b/9z/2//d/9z/4//l/+n/8P/t/+f/4f/W/9T/zv/G/8X/w/+8/7n/uP+3/7D/rf+t/6z/qP+i/6T/p/+r/7v/yv/N/8//0//R/9j/3v/k/+3/9f/7//z/AgAPAB0AJQAsADAALAAwADAALQApACkAKgAvADIAMQA0AD8ASABUAGQAbQB2AH0AggCQAJ4AogCxAMAAxgDMANEA2gDkAOoA8gD4APoA+QD3APMA9ADyAOoA6ADnAOIA4ADgAN0A3ADXANAAzQDLAMQAwQDCAL8AuACsAJ8AkwCKAIkAhgCDAIEAegBxAG4AagBjAFgAUQBDADUALgAmACMAHwAbABcAEgANAPr/5//f/+D/3v/X/9H/zv/K/8r/0f/R/8v/v//A/7j/sv+4/73/zf/V/9v/4f/m//L/+v/+/wkAGAAhACMALAA2ADQALgAsACwALgAtAC0ALwA1ADoAOwA5ADoAPQA+ADwAPAA9AD8APQA2ADUAMwA6AEEARgBOAFEAVQBgAGIAXwBgAGMAZgBgAFcATQBDAEEANgApACAAGwAWABAAAQDv/+3/7f/l/93/1P/J/77/sv+j/5r/kv+I/4T/f/+C/4f/iP+N/47/jf+H/3//gP+C/33/e/96/3//hP+K/47/i/+I/4X/hP+D/3v/cP9v/2j/Yf9h/17/Vv9S/1H/T/9W/13/Yv9m/2z/d/+D/5P/oP+r/7D/s/+2/7r/wf/H/83/1P/X/9j/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7//x//D/9v/3//L/8f8GABkAGQAaABoAIQAsADEAMgAvADYAOQA3AC0AKgAnACgAKgAdABcADwAGAPv/+P/3//r/8//u//b/+f8EAAMABwAAAPH/5//e/9T/0//a/9H/yP/E/7n/uf/B/77/xP/L/8X/wv/D/83/1P/V/9n/2v/g/+r/7P/0//j//v8GAAIA//8BAAQAAAD+/wEABwARABIAEgAaABwAHgAgACQALwAwACwAMQA3ADAAKwAnACcAIgAcABkAEwAUABAAEgAXAB0AIwAoADAANQA+AEAAOwA2ADMANAA1ADYALwAoACwAJAANAP7/9P/r/9b/xP++/6//qP+e/53/pf+k/6P/pP+q/6//qf+o/7T/uv+3/7H/s/+6/8X/yP/J/9P/3f/k/+n/7//5/wUADgAaACsANwBJAFEAUgBeAF4AYgBhAFUAVQBZAF4AXgBfAGUAZQBmAF4AWABZAFkATwBFAD4AOwA8AC8AJwAbABAAAwD3//T/8f/y/+z/5v/g/93/1P/Y/9//4v/i/93/2f/X/9T/2P/g/+P/8f/v/+n/9P/x/+b/5P/n/+3/7P/j/+P/6v/j/9//2v/a/9f/1v/e/+P/8f8EAAUACgAKAA0AEgAJAPn/7f/p/+T/3f/e/+T/3//T/8//z//K/8T/w//B/8T/yf/N/8//1P/Z/9j/1v/S/9X/2//e/9v/4P/e/9f/1v/T/9r/4//t//L//f8JABQAGwAgABkAEAAZABwAHwAhACYAJwAgABUACAAGAAkADAAKAA4AFQAaABQAGgAVABQADwAJAAwABwAJABAAEQAaAB8AIAAiACoAMgAtACoALwAmABgAGQAfAB0AFgAUABEADgAOAAwACAAGAAcABwAAAAsAGQAiAC0AKgAoACIAJwAuAC8AMgA4ADoAMQAqACMAJAAiAB8AFwABAPD/5f/b/9n/1P/T/9X/1P/V/+L/7f/7/wMAAgAOABgAFwAVABYAHAAaABYAGwAiAB8AGwARAAQABQALAAoABwD9//r//f8DAAMACAAfACgAJAAgACoALAAlACAAHAAdABsAFwASABIAFQATAA8AFQAaABAABwAAAPn/8//4//z/9v/8/wMAAwD9//z///8AAPL/7P/v//f//f/v/+b/6f/x/+//7P/q//X/9v/z//L/9/8CAAwAFgAQAAwADQAOAAsABgD/////+//u/+v/3P/Q/9D/w//A/8P/uv+z/6z/p/+r/6b/p/+v/6//r/+9/8H/xv/Z/9P/2f/p/+z/8v8AAAwAIAAkACoAOAA+AEUARgBJAFQAXABZAFUATwBLAEoASgBCADUALwArACcAIQAoADUAMAAuACEAGQAeACUAIgAsADYALgAvAC4ALQAvADMAMAAxACwAJAAhACIAJQAaABkAGQAaABcAEAAPAAwABAD+//n/8v/o/+j/6//n/+f/3//m/+j/2v/Q/9D/1P/W/8z/w//I/8n/wP+8/7b/t/+0/6j/o/+n/7X/v//G/8v/1f/U/8r/yP/I/7//vP+5/7n/tf+7/7//vP/G/8X/wv/C/8X/3f/q/+3/8v/7//z//f/y//L/AQAHAAgABQAHABEAMAApADAAMgAOAOX/6P/s/8j/zf/p/+D/2/8BABIAFgAjABoACwD//wcADgALABEAEgABAPL/7//q/+//8f/3//X/6v/k/9j/0v+7/6z/pf+c/5L/lP+m/7X/vv+9/8X/yv/B/7//x//R/9r/5P/x/wMAHgAiACsAOgBDAE0ATgBfAGsAbwBvAG4AewB6AHEAYwBbAFoAUABMAFcAXgBaAFQASABFAFQATgBCAEAAPwA2ACcAHgAeACIAEQAMAAcACAADAOf/1v/W/9r/zP/L/9D/zf/E/7r/rv+r/6j/m/+b/6L/p/+o/7T/vv++/8b/1v/g/+L/4f/c/9f/3P/p/+3/9//w/+r/+P/7/wUACAARABYAEQAUAB4AKQAxADIALgArADEASgBMAFwAYQBtAGoAeQCBAIUAkgCOAJoAmQCfAKUAowCdAJgAkwCPAJAAhwB+AHMAeQCAAH0AdwByAHMAcQB1AGMAVQBWAEwARQA1ADUAPAA7ACcAIAAUABUADAACAP3//P/5/+r/5f/d/9P/yv/E/7H/sf+p/6f/pf+Y/5L/if+B/3T/cf94/33/c/9u/2b/aP9q/27/c/94/3//d/93/3T/e/9+/3b/cv90/3P/b/9x/3b/fv+H/5P/m/+p/7X/wf/D/87/1//Y/93/7f/4//z/9f/v//7/AgD9//n/AQABAPb/8P/y//P/9P/y//P/AAANAAwACwAOABIAFQAQABcAEwATAAYADQATABEAEAAOAAkACQALAAMACgD7////EAATABgAFQAMAAwABwABAAkAAAACAP3//P/2//P/7P/v/+//3v/h/+3//f///wEA9/8BAAkAAgD8//f/+P/1//b/+f/7//X/8v/0//b/AAAJAAsACwALABAADwAJAAAAAAACAPz/8f/p/+j/7P/j/9T/zP/G/8j/uv++/7z/wv/C/8n/z//Q/9X/3P/d/9r/2P/V/9//4//t/+//AAAHAAgABwAIAA4AEQAZACQALwA0ADMALwAlACUAIwApACwAIQAmACgAKAApACgAKgAcABsAJAApADkAPgBAAEQASwBLAE0AVwBWAFsAWABaAFwAXABZAE0AUgBLAEUAOAAvACsALAAzAC8AMAAsAC4AJAAiAC0ANAA5ADkAPQA2ADEAKwAxADEAMQAzADIAKQApAC0AMQAyADEALwAqACoAJgAbACQAHQAbABsABwAFAAMA/f/x/+X/2//c/9z/1//U/9P/0f/N/8X/wP+6/73/zP/W/+L/6P/y//b//f8EAA0AJQA2ADoAMgA0ADUAMwA1AD8ASgBZAGAAYABuAGgAbgBoAGsAZwBqAGYAYwByAG0AbgBlAFwAWABOAEIAOAAzACgAIwAXABMABQAKAAMA8//g/9T/0v/N/87/zP/R/8f/xv+8/7n/tv+m/5z/mf+X/5L/lf+Z/6r/rP+k/6H/oP+k/6j/tP/A/8//0f/T/8f/w//A/7z/wf/C/8//yv/S/9b/3v/e/9j/2//Y/9f/3//k/+X/6//u/+X/3//i/+T/5f/k/+H/3v/e/9//2//X/9//3//g/9P/1f/S/8//y//E/8n/xv/E/8X/w//B/7//wf/K/9T/1//V/9j/2v/d/+D/5P/o//D/9P/u/+r/5//l/+H/4//e/93/2//i/+n/6v/u/+X/4P/i/+H/3v/T/83/zP/O/8n/v//E/8L/uv+p/6D/qP+w/7z/uv/I/9n/zP/H/8v/zP/R/9T/0v/S/8//zf/G/8b/zP/Y/8//0P/b/+L/7P/o/+z/+P8EAAkAFAAfACYANwA2ADoAPQBHAEoATgBhAGwAbwBsAG8AbwB1AG0AZABoAF4AYABbAFsAagB0AHcAeQBzAGYAXABRAD8AMwAfABUACgD6/+T/zv/P/8b/w/+5/7P/wP/A/8D/vv/D/9H/2v/Y/9r/7v/5/wEAAgADAA0AEQADAPn/+f/4//n/9P/8/woAEwAOABYAHgAcACUAJQAgACIAFQATABcAEAALAPz/8//v//P/8f/u/+L/3P/Z/8//zP/P/9L/1f/P/77/vf/H/83/x//I/8b/wf+2/7j/yv/Z/97/2f/e/+P/9v/7//7/CgAOABoAJQA/AFAAYABqAH0AiACLAJkAowCtALUAugDEAMEAwADFAMAAwwC+AMkAywDEALwAsgCyAKkAqgCgAJUAjACDAHQAXgBPAEoARQA7ADUAMQAtACwAHwAUABQAFQAaABsAGwAZABIADwAPAAsACAAAAAYABAACAAsADwAXABUAFQAOAA4ACwATABkAEgASABMAGQAcACIAHgAfABgADQAUABMAEQAaABAACAD//+//9v/0/+3/7P/e/9P/0f/P/87/zf+//7H/qv+g/5X/lP+I/4H/ff94/3b/ef9+/4X/j/+J/5X/nP+b/63/vP+//8v/0P/K/9r/3P/V/9r/4//r//b/8f/y//7/AgALAB0ALAA/AE0ASQBCADcANwA/AEUAPwA0ACsALgAtACoAIwAhABkACQD///f/+/8CAAIA8f/l/9r/xP+//7T/qP+a/4f/gP+D/4j/gf+B/3b/ff99/33/gP95/3v/c/9t/2j/a/9n/2n/c/9z/2v/dP92/27/cf9x/3j/f/+E/4P/k/+m/6j/uf/C/8r/0f/Y/97/5f/x//f/+v8AAAkABgAIAAcAAwALAAoAEQAdACgALAAmADEAPgA5AD0AQgBCAE8AUwBbAF8AYABWAEoASQBNAEMANQAtADUAPwBHAD8ALQAqACUAGgAOAPv/9f/s/+P/5f/h/9//2P/W/8//zv/V/9f/3v/V/9n/2//r//D/+P/7//b//P/8/wkACwAaACYAIgAeABUAFwAaABYAGAAcACMAHQAaABsAHAAmADAAKwAnACMAKgAvADEALwAlACkAHAAZACUANAA+AEAAQgA3ADcANgBDAEkATABSAFQARwBMAFUAVQBZAEsAQgA2ADwANwAwACkAIwAoACEAHgAXABUAGAAWABMA///9//L/4//m/9P/2f/f/+X/5v/Y/9H/xP/C/8n/1f/V/9T/1v/R/8v/yv/L/9H/4v/e/+L/5v/w//n/DAAVAB8AHAAaAB8AHgAnABoAGAAZABcAEwASABQADgAHAPr/9P/w//T/7//h/9z/1//j/9v/0v/R/9X/0//V/8//2//y//z/BQALABUAFAAfABsAGgAiAB8AKQA4AD8ATABTAFYAZQBiAGkAbABxAH8AgwCHAIMAfAB8AH8AfwCCAHsAeACCAHoAcQBnAFkAWABMAEMAQgAyACMAEwAFAP3/9P/n/+P/5v/m/+n/4v/S/87/w/+5/6z/p/+j/6T/ov+b/6b/uf/J/9D/yv/H/8r/zv/b/+f/6v/v//b/AgAOABUAJgAoADgASABWAFgAXQBuAHgAegBxAHEAaQBeAFsAVwBTAE0AQwAyAB4AHgAYAA0A+v/p/+f/7f/v/+r/7P/u/+f/2f/f/+v/7v/q/+D/3P/a/9n/1v/W/9f/0//E/7v/u/++/7n/s/+r/7L/tv+v/6r/r/+7/7v/s/+x/7n/wv/G/77/uf+1/7f/tf+4/8L/uv+y/6b/qP+q/7H/qP+m/6H/lv+S/5b/nv+i/5r/k/+c/6X/of+j/63/t/+8/7//x//K/8b/xf/H/8j/0v/X/9v/3f/V/8z/zP/N/8P/yv/S/9X/3f/d/9r/3f/W/9X/4f/f/+T/2f/W/9b/0v/N/8n/xP+9/7T/qP+k/6L/o/+Z/5D/gP97/3T/cP9r/23/c/9x/4D/hf+N/4//k/+V/5r/nP+a/5z/o/+x/7f/wv/I/9D/1v/d/+3/9P/y//j/BgAPAB8ALQAwADYASQBRAGAAbwB9AHwAdgB5AIUAhQCLAJQAmACVAIkAiwCGAJAAlQCUAJIAgwBzAHAAbgB0AHgAdQBwAGIAWQBYAFUAVgBOAE8AVABLAEQANwA2ADcAMQAnACIAIgAmACoALQA3ADwAOAA2AD0AQABHAEwATgBVAEwAPgBBAEwAVABaAFIAVwBcAGIAZQBmAGcAbAByAHAAcwB4AHcAeACAAIAAfQBwAGoAbgBvAHgAagBqAGgAZgBxAGwAdgBtAHMAcwBwAHcAdAB5AHkAdwB5AHQAeQB2AHUAgAB+AIUAiACIAIIAfgB4AHAAdgBpAFsAWABQAE8ARgBAAD0AOwA3ADMALgA4ADwAMQAmABcACAAGAP3//f/y/+b/4f/W/8v/wv+2/7P/tP+y/7D/s/+9/8j/0v/U/9P/1v/W/9r/3//o/+//8//6//r////7//b/5P/Z/9j/2P/i/9//1v/W/9L/y//F/77/wf/B/77/u//B/7z/s/+s/6P/p/+p/5z/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GAAdACMAKgAwADUANQAxAC4ALgAwADYAOwBAAEAAPwA/AEAAQgBFAEgASABHAEcATABTAFkAXQBfAF8AXQBaAFgAWABcAGEAYgBeAFkAVQBSAFAATwBSAFMAUABNAEoASwBRAFQAUgBOAEkAPwA0ACkAHwAYABAADAANABMAHgAkACcAKgApACkAJgAkACEAHgAXAA0ABQABAPv/7//m/+T/6P/r/+//8f/u/+r/6//v//j/+//5//r//f8HABEAFgAaABoAHgAmADAAOgBDAEgATQBRAFUAVgBWAE8AQgA5ADgAPgBCAEIARABHAEQAPgA8ADcAMAAsACUAGQAIAPf/6P/a/8z/wP+3/63/oP+X/5P/j/+N/4z/kP+W/5z/ov+n/6z/sf+6/8P/yf/P/9T/1v/Z/97/5//x//b/+/8CAAsAEwAWABkAGwAcABwAHQAgACAAGgAWABkAHQAcABcAFgAeACUAIwAWAAwACgAKAAQA+P/p/9//1//K/8D/u/+7/7T/q/+i/6D/p/+w/7f/u/++/7//vP+1/7L/uv/F/8v/0P/X/9//5P/q//T/+////wIADAAVABcAFgAZABkAFwASAAsABQABAAAA///7//H/6v/p/+v/5v/h/+D/4f/m/+b/5f/k/+T/4//h/97/3P/c/9n/1P/T/9T/2v/e/+X/6v/t//L/9f/5//v/+v/+/wMABwAKABAAFwAbACAAKAAvADIANQA1ADUANgA3ADYANQA2ADgAOwA7ADkANQAuACwAKwAsACwALQAoACAAGgAYABcAEgAJAAQABwAJAAQA///6//n/+P/3//r///8DAAQABAADAAEA/v8AAAQADAASABUAFgAcACIAJwAnACQAJgArADEANwA+AEUATQBQAFMAVgBbAF4AWwBZAFoAWABSAEsASQBLAEcAPQAzACkAHAASAAgA/f/1//T/8//u/+b/4f/h/+P/5f/l/9//1v/T/9T/0//R/9D/zf/K/8f/xP/A/7j/sf+r/6j/pP+k/6j/r/+z/7b/uP+4/7b/t/+7/8H/xP/B/8H/w//G/8f/yf/P/9T/2f/h/+b/6v/t//P/9//2//f/AAALAA8ADgAPABYAIQApADAANgA9AEIAQwBFAEcARQBAADoAOgA9AD8AOAAuACkAJwAmAB4AEQACAPn/8//x/+3/5P/b/9L/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9v/8//3/AAAJABcAJwA0ADwAQgBEAEQARQBEAD8AOgA4ADsAQwBMAFAAVABYAFsAWwBZAFcAVgBUAFIAUABNAFAAVgBdAFoAVABPAEsARgBBAD8APgBAAEMARQBIAE0AUQBPAEsARwBFAEUAQwA/ADoAMwAqAB4AFgATABIAFgAcAB8AIgAjACAAHgAcABgAEwARAA8ADwAKAAYABgACAPr/8//t/+r/5f/i/+D/4v/i/+P/4v/d/9T/x/+8/7L/q/+n/6T/ov+d/5j/kf+Q/5L/lP+U/5X/mf+e/6L/pv+u/7f/v//F/8//1v/b/+D/6P/v//H/9P/5//3/AwAOABQAFwAaAB4AIgAhAB0AHQAhAB4AFwASABIAEwAVABkAHAAiACkALQAwADMANQAzADMAOQA9AD4APAA3ADMANQA9AEEAQwBBAD8APgA/AD0AOQA4ADoAPgBHAE4AUwBQAEoARwBGAEIAOwAyACgAHgASAAgA/v/1//D/7v/o/97/1//R/8r/v/+z/6f/n/+c/5b/j/+I/4P/fv95/3b/df96/4D/iv+V/57/pP+n/6n/rP+u/6z/p/+o/6//u//E/8r/0P/V/9//6//y//f/+v/+/wIABAAFAAcACgAOABQAFwAYABcAFQAWABkAGAAWABQAFgAZABsAGAAQAAkACAALABAAEAANAAkABwAFAAAA+P/0//f//v8EAAsAFgAdACEAJQAsADEAMwA1ADUAMgAvACsAKQAkAB8AGwASAAoABwAIAAoACAAGAAYABQAAAP7//f/7//j/9P/u/+j/5v/q//P//f8JABQAHQAeABQABwD///n/+f/5//b/8f/r/+j/5f/k/+X/5f/i/9//3f/g/+b/7P/w//P/7//q/+T/4P/f/+H/5P/n/+j/4//f/+P/6//z//v/BAAMABIAEwATABQAFQAQAAgAAgABAAUABwAHAAcABQD+//b/8v/x//D/7f/s/+3/6//r/+r/5v/e/9b/zf/E/77/vv++/8H/xf/M/9f/4v/t//X//P8GABUAIwAuADgARABOAFMAUgBSAFAATgBPAFIAVABVAFUAUABGADwANgA1ADIALQAqACcAJwAoACkAKwAsAC4ALwAuAC4ALgAsACYAIQAiACAAGAAQAAwADAAQABEADgALAAsAEQAUABEADgAOAAsABAD7//f/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6P/3/wUAEgAWABYAFwAWABUAFAAcACMAJgAeABQACwAHAAQAAgD+//3//v/6//D/5//l/+P/3v/Z/9P/0P/V/97/5P/m/+f/5P/f/93/3P/a/9P/zf/K/8r/yP/C/7z/t/+0/7b/vv/I/87/z//P/9H/1//c/+L/6v/v//T/9P/y/+3/6v/o/+n/5//j/93/3P/e/+D/4f/i/+b/6P/r//D/9P/9/wUAEAAcACEAJgAuADgAPwBCAEQARgBGAEUARABFAEkARgBAADoAMgAuACwAMgA1ADUAOABAAEUARgBDAEUASwBPAFIAUgBXAFwAXQBeAFsAWABbAF4AYwBrAHcAhACMAJEAkQCOAIsAhQCAAHwAdQBuAGgAYQBZAFEATgBOAEwARwBAADYALAAdAAwA///5//j/9//z/+3/5//g/97/4P/l/+j/6v/w//L/9f/4//n/+f/6//v//f/7//r/9//1//X/8v/x//H/8v/2//r//P/8//z/+P/x/+n/4//f/9z/2P/V/9L/z//N/8//0//U/9X/2f/e/+D/4v/n/+j/6f/r/+//9v/9/wMABwAKABIAHAAlACsALgAzADwARABEAEIAQgBDAEQARQBGAEkASQBDADsALwAfAA8ABAD7//H/6v/l/+X/5f/l/+b/5P/i/+T/5P/h/+H/5v/r/+v/6f/l/+D/2//U/8//z//R/8//zf/H/8P/wP+7/7b/sv+v/6n/ov+b/5f/kv+M/4j/hv+F/4f/if+J/4X/gv9+/37/gP+B/4L/gP+B/4j/jP+Q/5b/of+y/8P/zf/P/9H/1//b/9//4f/i/+f/7P/t//D/7v/r/+f/4f/f/97/3//i/+f/7f/x/+//6P/l/+j/8f/9/woAEgAdACoAMgA4AD4ASABQAFQAVgBcAGMAaABoAGMAXQBbAFoAVwBSAE4ATgBNAEsASABFADwALgAiABsAFAAJAP//9//x/+r/3//U/8//zv/M/8b/v/+8/7v/uv+6/7r/uP+4/7f/uP+5/7v/wP/G/8r/zv/S/9f/3P/e/+H/5P/m/+f/6f/y//z/AwAIAAsADgASABQAFQAUABQAFwAeACIAIAAaABcAFgATAAcA/v/+/wMABAADAAcACwAOABAAFAAZABoAGQAaABwAHAAYABYAGQAeACUALQA3AEYAVgBjAG4AeQCBAIkA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kf+b/7r/uP+n/63/wv/k//j/6f8gAD0AJwD6//H/BwD6//n/4v/V/9j/zP8DAAQADQAcAA0ADwBQAFMAZwApAPf/BgD3//r//P8DAP//AgAFAAkACQAFAAEA/v/7//n/+v/5//3/AAABAAIABgAHAAcABgAHAAkACAAHAAsACwALAA4ADgAOAA0ACwAJAAYAAwABAAAA//8AAAIAAgAEAAYABwAHAAgACAAHAAUAAwAAAP7//P/6//n/+f/4//j/+P/5//r//P/+/wAAAQACAAMAAwACAAEA///+//7//f/9////AQAGAAoADQASABUAFgAXABYAFAARAA4ACwAIAAUABAADAAMAAwADAAQABAAEAAMAAgAAAP7//P/6//j/9//2//T/9P/1//X/9f/1//b/9//5//v//f///wEAAgADAAQABAADAAMAAwACAAIAAwAEAAYACQALAA4AEAARABIAEQAPAA0ACQAHAAQAAQD///7//v//////AAABAAIAAgABAAEAAAD///7//f/8//z/+//7//z//P/9//3//v///wAAAQACAAQABAAEAAMAAwADAAIAAQABAAAAAAAAAAEAAQACAAQABAAFAAUABgAGAAUABQAFAAMAAwAEAAIAAgACAAIAAgABAAAAAAD///3//v/+//7//v///wEAAwAFAAQAAQD9//3//P///wEABAD6//3/BwACAAUA/P///wEA/f/9/wIA/v8FAAMAAAD///3/AQD+////AwACAP3/+f8BAAQAAgD///7/AQADAAEAAQAAAP//AwADAP///v8CAAIA///+/wAAAQAAAAAAAQAAAAAAAQAAAAAAAAA='; diff --git a/packages/ai/package.json b/packages/ai/package.json index 468f166ad7e..b36081aa8f0 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,14 +1,14 @@ { "name": "@firebase/ai", - "version": "1.4.0", + "version": "2.3.0", "description": "The Firebase AI SDK", "author": "Firebase (https://firebase.google.com/)", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/ai-public.d.ts", @@ -18,9 +18,9 @@ }, "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/esm/index.esm2017.js" + "import": "./dist/esm/index.esm.js" }, - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -35,13 +35,16 @@ "dev": "rollup -c -w", "update-responses": "../../scripts/update_vertexai_responses.sh", "testsetup": "yarn update-responses && yarn ts-node ./test-utils/convert-mocks.ts", - "test": "run-p --npm-path npm lint test:browser", + "test": "run-p --npm-path npm lint type-check test:browser", "test:ci": "yarn testsetup && node ../../scripts/run_tests_in_ci.js -s test", "test:skip-clone": "karma start", "test:browser": "yarn testsetup && karma start", + "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register --require src/index.node.ts 'src/**/!(*-browser)*.test.ts' --config ../../config/mocharc.node.js", "test:integration": "karma start --integration", + "test:integration:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha integration/**/*.test.ts --config ../../config/mocharc.node.js", "api-report": "api-extractor run --local --verbose", "typings:public": "node ../../scripts/build/use_typings.js ./dist/ai-public.d.ts", + "type-check": "yarn tsc --noEmit", "trusted-type-check": "tsec -p tsconfig.json --noEmit" }, "peerDependencies": { @@ -50,14 +53,14 @@ }, "dependencies": { "@firebase/app-check-interop-types": "0.3.3", - "@firebase/component": "0.6.17", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "@rollup/plugin-json": "6.1.0", "rollup": "2.79.2", "rollup-plugin-replace": "2.2.0", diff --git a/packages/ai/rollup.config.js b/packages/ai/rollup.config.js index 3b155335898..7ebbff4f2f5 100644 --- a/packages/ai/rollup.config.js +++ b/packages/ai/rollup.config.js @@ -39,7 +39,7 @@ const buildPlugins = [ 'integration' ], compilerOptions: { - target: 'es2017' + target: 'es2020' } } }), @@ -57,7 +57,7 @@ const browserBuilds = [ plugins: [ ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('esm', 2017), + ...generateBuildTargetReplaceConfig('esm', 2020), __PACKAGE_VERSION__: pkg.version }), emitModulePackageFile() @@ -74,7 +74,7 @@ const browserBuilds = [ plugins: [ ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('cjs', 2017), + ...generateBuildTargetReplaceConfig('cjs', 2020), __PACKAGE_VERSION__: pkg.version }) ], @@ -93,7 +93,7 @@ const nodeBuilds = [ plugins: [ ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('esm', 2017) + ...generateBuildTargetReplaceConfig('esm', 2020) }) ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) @@ -108,7 +108,7 @@ const nodeBuilds = [ plugins: [ ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('cjs', 2017) + ...generateBuildTargetReplaceConfig('cjs', 2020) }) ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) diff --git a/packages/ai/src/api.test.ts b/packages/ai/src/api.test.ts index 27237b4edd3..65ecbbdcba8 100644 --- a/packages/ai/src/api.test.ts +++ b/packages/ai/src/api.test.ts @@ -16,12 +16,20 @@ */ import { ImagenModelParams, ModelParams, AIErrorCode } from './types'; import { AIError } from './errors'; -import { ImagenModel, getGenerativeModel, getImagenModel } from './api'; +import { + getAI, + ImagenModel, + LiveGenerativeModel, + getGenerativeModel, + getImagenModel, + getLiveGenerativeModel +} from './api'; import { expect } from 'chai'; import { AI } from './public-types'; import { GenerativeModel } from './models/generative-model'; -import { VertexAIBackend } from './backend'; -import { AI_TYPE } from './constants'; +import { GoogleAIBackend, VertexAIBackend } from './backend'; +import { getFullApp } from '../test-utils/get-fake-firebase-services'; +import { AI_TYPE, DEFAULT_HYBRID_IN_CLOUD_MODEL } from './constants'; const fakeAI: AI = { app: { @@ -38,6 +46,40 @@ const fakeAI: AI = { }; describe('Top level API', () => { + describe('getAI()', () => { + it('works without options', () => { + const ai = getAI(getFullApp()); + expect(ai.backend).to.be.instanceOf(GoogleAIBackend); + }); + it('works with options: no backend, limited use token', () => { + const ai = getAI(getFullApp(), { useLimitedUseAppCheckTokens: true }); + expect(ai.backend).to.be.instanceOf(GoogleAIBackend); + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.true; + }); + it('works with options: backend specified, limited use token', () => { + const ai = getAI(getFullApp(), { + backend: new VertexAIBackend('us-central1'), + useLimitedUseAppCheckTokens: true + }); + expect(ai.backend).to.be.instanceOf(VertexAIBackend); + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.true; + }); + it('works with options: appCheck option is falsy', () => { + const ai = getAI(getFullApp(), { + backend: new VertexAIBackend('us-central1'), + useLimitedUseAppCheckTokens: undefined + }); + expect(ai.backend).to.be.instanceOf(VertexAIBackend); + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.false; + }); + it('works with options: backend specified only', () => { + const ai = getAI(getFullApp(), { + backend: new VertexAIBackend('us-central1') + }); + expect(ai.backend).to.be.instanceOf(VertexAIBackend); + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.false; + }); + }); it('getGenerativeModel throws if no model is provided', () => { try { getGenerativeModel(fakeAI, {} as ModelParams); @@ -102,6 +144,21 @@ describe('Top level API', () => { expect(genModel).to.be.an.instanceOf(GenerativeModel); expect(genModel.model).to.equal('publishers/google/models/my-model'); }); + it('getGenerativeModel with HybridParams sets a default model', () => { + const genModel = getGenerativeModel(fakeAI, { + mode: 'only_on_device' + }); + expect(genModel.model).to.equal( + `publishers/google/models/${DEFAULT_HYBRID_IN_CLOUD_MODEL}` + ); + }); + it('getGenerativeModel with HybridParams honors a model override', () => { + const genModel = getGenerativeModel(fakeAI, { + mode: 'prefer_on_device', + inCloudParams: { model: 'my-model' } + }); + expect(genModel.model).to.equal('publishers/google/models/my-model'); + }); it('getImagenModel throws if no model is provided', () => { try { getImagenModel(fakeAI, {} as ImagenModelParams); @@ -166,4 +223,62 @@ describe('Top level API', () => { expect(genModel).to.be.an.instanceOf(ImagenModel); expect(genModel.model).to.equal('publishers/google/models/my-model'); }); + + it('getLiveGenerativeModel throws if no apiKey is provided', () => { + const fakeVertexNoApiKey = { + ...fakeAI, + app: { options: { projectId: 'my-project', appId: 'my-appid' } } + } as AI; + try { + getLiveGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_API_KEY); + expect((e as AIError).message).equals( + `AI: The "apiKey" field is empty in the local ` + + `Firebase config. Firebase AI requires this field to` + + ` contain a valid API key. (${AI_TYPE}/${AIErrorCode.NO_API_KEY})` + ); + } + }); + it('getLiveGenerativeModel throws if no projectId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', appId: 'my-appid' } } + } as AI; + try { + getLiveGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_PROJECT_ID); + expect((e as AIError).message).equals( + `AI: The "projectId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid project ID. (${AI_TYPE}/${AIErrorCode.NO_PROJECT_ID})` + ); + } + }); + it('getLiveGenerativeModel throws if no appId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', projectId: 'my-project' } } + } as AI; + try { + getLiveGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_APP_ID); + expect((e as AIError).message).equals( + `AI: The "appId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid app ID. (${AI_TYPE}/${AIErrorCode.NO_APP_ID})` + ); + } + }); + it('getLiveGenerativeModel gets a LiveGenerativeModel', () => { + const liveGenerativeModel = getLiveGenerativeModel(fakeAI, { + model: 'my-model' + }); + expect(liveGenerativeModel).to.be.an.instanceOf(LiveGenerativeModel); + expect(liveGenerativeModel.model).to.equal( + 'publishers/google/models/my-model' + ); + }); }); diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts index d2229c067fc..6e56aea793c 100644 --- a/packages/ai/src/api.ts +++ b/packages/ai/src/api.ts @@ -18,49 +18,39 @@ import { FirebaseApp, getApp, _getProvider } from '@firebase/app'; import { Provider } from '@firebase/component'; import { getModularInstance } from '@firebase/util'; -import { AI_TYPE } from './constants'; +import { AI_TYPE, DEFAULT_HYBRID_IN_CLOUD_MODEL } from './constants'; import { AIService } from './service'; -import { AI, AIOptions, VertexAI, VertexAIOptions } from './public-types'; +import { AI, AIOptions } from './public-types'; import { ImagenModelParams, + HybridParams, ModelParams, RequestOptions, - AIErrorCode + AIErrorCode, + LiveModelParams } from './types'; import { AIError } from './errors'; -import { AIModel, GenerativeModel, ImagenModel } from './models'; +import { + AIModel, + GenerativeModel, + LiveGenerativeModel, + ImagenModel +} from './models'; import { encodeInstanceIdentifier } from './helpers'; -import { GoogleAIBackend, VertexAIBackend } from './backend'; +import { GoogleAIBackend } from './backend'; +import { WebSocketHandlerImpl } from './websocket'; export { ChatSession } from './methods/chat-session'; +export { LiveSession } from './methods/live-session'; export * from './requests/schema-builder'; export { ImagenImageFormat } from './requests/imagen-image-format'; -export { AIModel, GenerativeModel, ImagenModel, AIError }; +export { AIModel, GenerativeModel, LiveGenerativeModel, ImagenModel, AIError }; export { Backend, VertexAIBackend, GoogleAIBackend } from './backend'; - -export { AIErrorCode as VertexAIErrorCode }; - -/** - * @deprecated Use the new {@link AIModel} instead. The Vertex AI in Firebase SDK has been - * replaced with the Firebase AI SDK to accommodate the evolving set of supported features and - * services. For migration details, see the {@link https://firebase.google.com/docs/vertex-ai/migrate-to-latest-sdk | migration guide}. - * - * Base class for Firebase AI model APIs. - * - * @public - */ -export const VertexAIModel = AIModel; - -/** - * @deprecated Use the new {@link AIError} instead. The Vertex AI in Firebase SDK has been - * replaced with the Firebase AI SDK to accommodate the evolving set of supported features and - * services. For migration details, see the {@link https://firebase.google.com/docs/vertex-ai/migrate-to-latest-sdk | migration guide}. - * - * Error class for the Firebase AI SDK. - * - * @public - */ -export const VertexAIError = AIError; +export { + startAudioConversation, + AudioConversationController, + StartAudioConversationOptions +} from './methods/live-session-helpers'; declare module '@firebase/component' { interface NameServiceMapping { @@ -68,35 +58,6 @@ declare module '@firebase/component' { } } -/** - * @deprecated Use the new {@link getAI | getAI()} instead. The Vertex AI in Firebase SDK has been - * replaced with the Firebase AI SDK to accommodate the evolving set of supported features and - * services. For migration details, see the {@link https://firebase.google.com/docs/vertex-ai/migrate-to-latest-sdk | migration guide}. - * - * Returns a {@link VertexAI} instance for the given app, configured to use the - * Vertex AI Gemini API. This instance will be - * configured to use the Vertex AI Gemini API. - * - * @param app - The {@link @firebase/app#FirebaseApp} to use. - * @param options - Options to configure the Vertex AI instance, including the location. - * - * @public - */ -export function getVertexAI( - app: FirebaseApp = getApp(), - options?: VertexAIOptions -): VertexAI { - app = getModularInstance(app); - // Dependencies - const AIProvider: Provider<'AI'> = _getProvider(app, AI_TYPE); - - const backend = new VertexAIBackend(options?.location); - const identifier = encodeInstanceIdentifier(backend); - return AIProvider.getImmediate({ - identifier - }); -} - /** * Returns the default {@link AI} instance that is associated with the provided * {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new instance with the @@ -125,18 +86,25 @@ export function getVertexAI( * * @public */ -export function getAI( - app: FirebaseApp = getApp(), - options: AIOptions = { backend: new GoogleAIBackend() } -): AI { +export function getAI(app: FirebaseApp = getApp(), options?: AIOptions): AI { app = getModularInstance(app); // Dependencies const AIProvider: Provider<'AI'> = _getProvider(app, AI_TYPE); - const identifier = encodeInstanceIdentifier(options.backend); - return AIProvider.getImmediate({ + const backend = options?.backend ?? new GoogleAIBackend(); + + const finalOptions: Omit = { + useLimitedUseAppCheckTokens: options?.useLimitedUseAppCheckTokens ?? false + }; + + const identifier = encodeInstanceIdentifier(backend); + const aiInstance = AIProvider.getImmediate({ identifier }); + + aiInstance.options = finalOptions; + + return aiInstance; } /** @@ -147,16 +115,38 @@ export function getAI( */ export function getGenerativeModel( ai: AI, - modelParams: ModelParams, + modelParams: ModelParams | HybridParams, requestOptions?: RequestOptions ): GenerativeModel { - if (!modelParams.model) { + // Uses the existence of HybridParams.mode to clarify the type of the modelParams input. + const hybridParams = modelParams as HybridParams; + let inCloudParams: ModelParams; + if (hybridParams.mode) { + inCloudParams = hybridParams.inCloudParams || { + model: DEFAULT_HYBRID_IN_CLOUD_MODEL + }; + } else { + inCloudParams = modelParams as ModelParams; + } + + if (!inCloudParams.model) { throw new AIError( AIErrorCode.NO_MODEL, `Must provide a model name. Example: getGenerativeModel({ model: 'my-model-name' })` ); } - return new GenerativeModel(ai, modelParams, requestOptions); + + /** + * An AIService registered by index.node.ts will not have a + * chromeAdapterFactory() method. + */ + const chromeAdapter = (ai as AIService).chromeAdapterFactory?.( + hybridParams.mode, + typeof window === 'undefined' ? undefined : window, + hybridParams.onDeviceParams + ); + + return new GenerativeModel(ai, inCloudParams, requestOptions, chromeAdapter); } /** @@ -171,7 +161,7 @@ export function getGenerativeModel( * @throws If the `apiKey` or `projectId` fields are missing in your * Firebase config. * - * @beta + * @public */ export function getImagenModel( ai: AI, @@ -186,3 +176,29 @@ export function getImagenModel( } return new ImagenModel(ai, modelParams, requestOptions); } + +/** + * Returns a {@link LiveGenerativeModel} class for real-time, bidirectional communication. + * + * The Live API is only supported in modern browser windows and Node >= 22. + * + * @param ai - An {@link AI} instance. + * @param modelParams - Parameters to use when setting up a {@link LiveSession}. + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + * + * @beta + */ +export function getLiveGenerativeModel( + ai: AI, + modelParams: LiveModelParams +): LiveGenerativeModel { + if (!modelParams.model) { + throw new AIError( + AIErrorCode.NO_MODEL, + `Must provide a model name for getLiveGenerativeModel. Example: getLiveGenerativeModel(ai, { model: 'my-model-name' })` + ); + } + const webSocketHandler = new WebSocketHandlerImpl(); + return new LiveGenerativeModel(ai, modelParams, webSocketHandler); +} diff --git a/packages/ai/src/backwards-compatbility.test.ts b/packages/ai/src/backwards-compatbility.test.ts deleted file mode 100644 index 62463009b24..00000000000 --- a/packages/ai/src/backwards-compatbility.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { - AIError, - AIModel, - GenerativeModel, - VertexAIError, - VertexAIErrorCode, - VertexAIModel, - getGenerativeModel, - getImagenModel -} from './api'; -import { AI, VertexAI, AIErrorCode } from './public-types'; -import { VertexAIBackend } from './backend'; - -function assertAssignable(): void {} - -const fakeAI: AI = { - app: { - name: 'DEFAULT', - automaticDataCollectionEnabled: true, - options: { - apiKey: 'key', - projectId: 'my-project', - appId: 'app-id' - } - }, - backend: new VertexAIBackend('us-central1'), - location: 'us-central1' -}; - -const fakeVertexAI: VertexAI = fakeAI; - -describe('backwards-compatible types', () => { - it('AI is backwards compatible with VertexAI', () => { - assertAssignable(); - }); - it('AIError is backwards compatible with VertexAIError', () => { - assertAssignable(); - const err = new VertexAIError(VertexAIErrorCode.ERROR, ''); - expect(err).instanceOf(AIError); - expect(err).instanceOf(VertexAIError); - }); - it('AIErrorCode is backwards compatible with VertexAIErrorCode', () => { - assertAssignable(); - const errCode = AIErrorCode.ERROR; - expect(errCode).to.equal(VertexAIErrorCode.ERROR); - }); - it('AIModel is backwards compatible with VertexAIModel', () => { - assertAssignable(); - - const model = new GenerativeModel(fakeAI, { model: 'model-name' }); - expect(model).to.be.instanceOf(AIModel); - expect(model).to.be.instanceOf(VertexAIModel); - }); -}); - -describe('backward-compatible functions', () => { - it('getGenerativeModel', () => { - const model = getGenerativeModel(fakeVertexAI, { model: 'model-name' }); - expect(model).to.be.instanceOf(AIModel); - expect(model).to.be.instanceOf(VertexAIModel); - }); - it('getImagenModel', () => { - const model = getImagenModel(fakeVertexAI, { model: 'model-name' }); - expect(model).to.be.instanceOf(AIModel); - expect(model).to.be.instanceOf(VertexAIModel); - }); -}); diff --git a/packages/ai/src/constants.ts b/packages/ai/src/constants.ts index cb54567735a..82482527f3b 100644 --- a/packages/ai/src/constants.ts +++ b/packages/ai/src/constants.ts @@ -21,7 +21,7 @@ export const AI_TYPE = 'AI'; export const DEFAULT_LOCATION = 'us-central1'; -export const DEFAULT_BASE_URL = 'https://firebasevertexai.googleapis.com'; +export const DEFAULT_DOMAIN = 'firebasevertexai.googleapis.com'; export const DEFAULT_API_VERSION = 'v1beta'; @@ -30,3 +30,8 @@ export const PACKAGE_VERSION = version; export const LANGUAGE_TAG = 'gl-js'; export const DEFAULT_FETCH_TIMEOUT_MS = 180 * 1000; + +/** + * Defines the name of the default in-cloud model to use for hybrid inference. + */ +export const DEFAULT_HYBRID_IN_CLOUD_MODEL = 'gemini-2.0-flash-lite'; diff --git a/packages/ai/src/errors.ts b/packages/ai/src/errors.ts index 2e9787d0bf2..8190510d0d8 100644 --- a/packages/ai/src/errors.ts +++ b/packages/ai/src/errors.ts @@ -28,7 +28,7 @@ export class AIError extends FirebaseError { /** * Constructs a new instance of the `AIError` class. * - * @param code - The error code from {@link AIErrorCode}. + * @param code - The error code from {@link (AIErrorCode:type)}. * @param message - A human-readable message describing the error. * @param customErrorData - Optional error data. */ diff --git a/packages/ai/src/factory-browser.ts b/packages/ai/src/factory-browser.ts new file mode 100644 index 00000000000..98b91812397 --- /dev/null +++ b/packages/ai/src/factory-browser.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ComponentContainer, + InstanceFactoryOptions +} from '@firebase/component'; +import { AIError } from './errors'; +import { decodeInstanceIdentifier } from './helpers'; +import { chromeAdapterFactory } from './methods/chrome-adapter'; +import { AIService } from './service'; +import { AIErrorCode } from './types'; + +export function factory( + container: ComponentContainer, + { instanceIdentifier }: InstanceFactoryOptions +): AIService { + if (!instanceIdentifier) { + throw new AIError( + AIErrorCode.ERROR, + 'AIService instance identifier is undefined.' + ); + } + + const backend = decodeInstanceIdentifier(instanceIdentifier); + + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app').getImmediate(); + const auth = container.getProvider('auth-internal'); + const appCheckProvider = container.getProvider('app-check-internal'); + + return new AIService( + app, + backend, + auth, + appCheckProvider, + chromeAdapterFactory + ); +} diff --git a/packages/ai/src/googleai-mappers.ts b/packages/ai/src/googleai-mappers.ts index 23c238c1e3b..c6656c8318d 100644 --- a/packages/ai/src/googleai-mappers.ts +++ b/packages/ai/src/googleai-mappers.ts @@ -176,7 +176,7 @@ export function mapGenerateContentCandidates( // Throw early since developers may send a long video as input and only expect to pay // for inference on a small portion of the video. if ( - candidate.content?.parts.some( + candidate.content?.parts?.some( part => (part as InlineDataPart)?.videoMetadata ) ) { @@ -193,7 +193,8 @@ export function mapGenerateContentCandidates( finishMessage: candidate.finishMessage, safetyRatings: mappedSafetyRatings, citationMetadata, - groundingMetadata: candidate.groundingMetadata + groundingMetadata: candidate.groundingMetadata, + urlContextMetadata: candidate.urlContextMetadata }; mappedCandidates.push(mappedCandidate); }); diff --git a/packages/ai/src/index.node.ts b/packages/ai/src/index.node.ts index 1908e65b1cd..bb05fdcea45 100644 --- a/packages/ai/src/index.node.ts +++ b/packages/ai/src/index.node.ts @@ -55,7 +55,7 @@ function registerAI(): void { ); registerVersion(name, version, 'node'); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 8451d68bbf0..9d787c832dd 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -22,46 +22,27 @@ */ import { registerVersion, _registerComponent } from '@firebase/app'; -import { AIService } from './service'; import { AI_TYPE } from './constants'; import { Component, ComponentType } from '@firebase/component'; import { name, version } from '../package.json'; -import { decodeInstanceIdentifier } from './helpers'; -import { AIError } from './api'; -import { AIErrorCode } from './types'; +import { LanguageModel } from './types/language-model'; +import { factory } from './factory-browser'; declare global { interface Window { - [key: string]: unknown; + LanguageModel: LanguageModel; } } function registerAI(): void { _registerComponent( - new Component( - AI_TYPE, - (container, { instanceIdentifier }) => { - if (!instanceIdentifier) { - throw new AIError( - AIErrorCode.ERROR, - 'AIService instance identifier is undefined.' - ); - } - - const backend = decodeInstanceIdentifier(instanceIdentifier); - - // getImmediate for FirebaseApp will always succeed - const app = container.getProvider('app').getImmediate(); - const auth = container.getProvider('auth-internal'); - const appCheckProvider = container.getProvider('app-check-internal'); - return new AIService(app, backend, auth, appCheckProvider); - }, - ComponentType.PUBLIC - ).setMultipleInstances(true) + new Component(AI_TYPE, factory, ComponentType.PUBLIC).setMultipleInstances( + true + ) ); registerVersion(name, version); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/ai/src/methods/chat-session-helpers.test.ts b/packages/ai/src/methods/chat-session-helpers.test.ts index feab9fc3b05..e64f3e84e2d 100644 --- a/packages/ai/src/methods/chat-session-helpers.test.ts +++ b/packages/ai/src/methods/chat-session-helpers.test.ts @@ -22,7 +22,11 @@ import { FirebaseError } from '@firebase/util'; describe('chat-session-helpers', () => { describe('validateChatHistory', () => { - const TCS: Array<{ history: Content[]; isValid: boolean }> = [ + const TCS: Array<{ + history: Content[]; + isValid: boolean; + errorShouldInclude?: string; + }> = [ { history: [{ role: 'user', parts: [{ text: 'hi' }] }], isValid: true @@ -99,19 +103,23 @@ describe('chat-session-helpers', () => { { //@ts-expect-error history: [{ role: 'user', parts: '' }], + errorShouldInclude: `array of Parts`, isValid: false }, { //@ts-expect-error history: [{ role: 'user' }], + errorShouldInclude: `array of Parts`, isValid: false }, { history: [{ role: 'user', parts: [] }], + errorShouldInclude: `at least one part`, isValid: false }, { history: [{ role: 'model', parts: [{ text: 'hi' }] }], + errorShouldInclude: `model`, isValid: false }, { @@ -125,6 +133,7 @@ describe('chat-session-helpers', () => { ] } ], + errorShouldInclude: `function`, isValid: false }, { @@ -132,6 +141,7 @@ describe('chat-session-helpers', () => { { role: 'user', parts: [{ text: 'hi' }] }, { role: 'user', parts: [{ text: 'hi' }] } ], + errorShouldInclude: `can't follow 'user'`, isValid: false }, { @@ -140,6 +150,45 @@ describe('chat-session-helpers', () => { { role: 'model', parts: [{ text: 'hi' }] }, { role: 'model', parts: [{ text: 'hi' }] } ], + errorShouldInclude: `can't follow 'model'`, + isValid: false + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [ + { text: 'hi' }, + { + text: 'thought about hi', + thought: true, + thoughtSignature: 'thought signature' + } + ] + } + ], + isValid: true + }, + { + history: [ + { + role: 'user', + parts: [{ text: 'hi', thought: true, thoughtSignature: 'sig' }] + }, + { + role: 'model', + parts: [ + { text: 'hi' }, + { + text: 'thought about hi', + thought: true, + thoughtSignature: 'thought signature' + } + ] + } + ], + errorShouldInclude: 'thought', isValid: false } ]; @@ -149,7 +198,14 @@ describe('chat-session-helpers', () => { if (tc.isValid) { expect(fn).to.not.throw(); } else { - expect(fn).to.throw(FirebaseError); + try { + fn(); + } catch (e) { + expect(e).to.be.instanceOf(FirebaseError); + if (e instanceof FirebaseError && tc.errorShouldInclude) { + expect(e.message).to.include(tc.errorShouldInclude); + } + } } }); }); diff --git a/packages/ai/src/methods/chat-session-helpers.ts b/packages/ai/src/methods/chat-session-helpers.ts index 1bb0e2798f2..3c0c58b7bf5 100644 --- a/packages/ai/src/methods/chat-session-helpers.ts +++ b/packages/ai/src/methods/chat-session-helpers.ts @@ -24,13 +24,15 @@ const VALID_PART_FIELDS: Array = [ 'text', 'inlineData', 'functionCall', - 'functionResponse' + 'functionResponse', + 'thought', + 'thoughtSignature' ]; const VALID_PARTS_PER_ROLE: { [key in Role]: Array } = { user: ['text', 'inlineData'], function: ['functionResponse'], - model: ['text', 'functionCall'], + model: ['text', 'functionCall', 'thought', 'thoughtSignature'], // System instructions shouldn't be in history anyway. system: ['text'] }; @@ -65,7 +67,7 @@ export function validateChatHistory(history: Content[]): void { if (!Array.isArray(parts)) { throw new AIError( AIErrorCode.INVALID_CONTENT, - `Content should have 'parts' but property with an array of Parts` + `Content should have 'parts' property with an array of Parts` ); } @@ -80,7 +82,11 @@ export function validateChatHistory(history: Content[]): void { text: 0, inlineData: 0, functionCall: 0, - functionResponse: 0 + functionResponse: 0, + thought: 0, + thoughtSignature: 0, + executableCode: 0, + codeExecutionResult: 0 }; for (const part of parts) { diff --git a/packages/ai/src/methods/chat-session.test.ts b/packages/ai/src/methods/chat-session.test.ts index 0564aa84ed6..e92aa057af1 100644 --- a/packages/ai/src/methods/chat-session.test.ts +++ b/packages/ai/src/methods/chat-session.test.ts @@ -20,10 +20,11 @@ import { match, restore, stub, useFakeTimers } from 'sinon'; import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; import * as generateContentMethods from './generate-content'; -import { GenerateContentStreamResult } from '../types'; +import { Content, GenerateContentStreamResult, InferenceMode } from '../types'; import { ChatSession } from './chat-session'; import { ApiSettings } from '../types/internal'; import { VertexAIBackend } from '../backend'; +import { ChromeAdapterImpl } from './chrome-adapter'; use(sinonChai); use(chaiAsPromised); @@ -36,6 +37,12 @@ const fakeApiSettings: ApiSettings = { backend: new VertexAIBackend() }; +const fakeChromeAdapter = new ChromeAdapterImpl( + // @ts-expect-error + undefined, + InferenceMode.PREFER_ON_DEVICE +); + describe('ChatSession', () => { afterEach(() => { restore(); @@ -46,7 +53,11 @@ describe('ChatSession', () => { generateContentMethods, 'generateContent' ).rejects('generateContent failed'); - const chatSession = new ChatSession(fakeApiSettings, 'a-model'); + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + fakeChromeAdapter + ); await expect(chatSession.sendMessage('hello')).to.be.rejected; expect(generateContentStub).to.be.calledWith( fakeApiSettings, @@ -54,6 +65,53 @@ describe('ChatSession', () => { match.any ); }); + it('adds message and response to history', async () => { + const fakeContent: Content = { + role: 'model', + parts: [ + { text: 'hi' }, + { + text: 'thought about hi', + thoughtSignature: 'thought signature' + } + ] + }; + const fakeResponse = { + candidates: [ + { + index: 1, + content: fakeContent + } + ] + }; + const generateContentStub = stub( + generateContentMethods, + 'generateContent' + ).resolves({ + // @ts-ignore + response: fakeResponse + }); + const chatSession = new ChatSession(fakeApiSettings, 'a-model'); + const result = await chatSession.sendMessage('hello'); + // @ts-ignore + expect(result.response).to.equal(fakeResponse); + // Test: stores history correctly? + const history = await chatSession.getHistory(); + expect(history[0].role).to.equal('user'); + expect(history[0].parts[0].text).to.equal('hello'); + expect(history[1]).to.deep.equal(fakeResponse.candidates[0].content); + // Test: sends history correctly? + await chatSession.sendMessage('hello 2'); + expect(generateContentStub.args[1][2].contents[0].parts[0].text).to.equal( + 'hello' + ); + expect(generateContentStub.args[1][2].contents[1]).to.deep.equal( + fakeResponse.candidates[0].content + ); + expect(generateContentStub.args[1][2].contents[2].parts[0].text).to.equal( + 'hello 2' + ); + }); }); describe('sendMessageStream()', () => { it('generateContentStream errors should be catchable', async () => { @@ -63,7 +121,11 @@ describe('ChatSession', () => { generateContentMethods, 'generateContentStream' ).rejects('generateContentStream failed'); - const chatSession = new ChatSession(fakeApiSettings, 'a-model'); + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + fakeChromeAdapter + ); await expect(chatSession.sendMessageStream('hello')).to.be.rejected; expect(generateContentStreamStub).to.be.calledWith( fakeApiSettings, @@ -82,7 +144,11 @@ describe('ChatSession', () => { generateContentMethods, 'generateContentStream' ).resolves({} as unknown as GenerateContentStreamResult); - const chatSession = new ChatSession(fakeApiSettings, 'a-model'); + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + fakeChromeAdapter + ); await chatSession.sendMessageStream('hello'); expect(generateContentStreamStub).to.be.calledWith( fakeApiSettings, diff --git a/packages/ai/src/methods/chat-session.ts b/packages/ai/src/methods/chat-session.ts index 60794001e37..dac16430b7a 100644 --- a/packages/ai/src/methods/chat-session.ts +++ b/packages/ai/src/methods/chat-session.ts @@ -30,6 +30,7 @@ import { validateChatHistory } from './chat-session-helpers'; import { generateContent, generateContentStream } from './generate-content'; import { ApiSettings } from '../types/internal'; import { logger } from '../logger'; +import { ChromeAdapter } from '../types/chrome-adapter'; /** * Do not log a message for this error. @@ -50,6 +51,7 @@ export class ChatSession { constructor( apiSettings: ApiSettings, public model: string, + private chromeAdapter?: ChromeAdapter, public params?: StartChatParams, public requestOptions?: RequestOptions ) { @@ -95,6 +97,7 @@ export class ChatSession { this._apiSettings, this.model, generateContentRequest, + this.chromeAdapter, this.requestOptions ) ) @@ -146,6 +149,7 @@ export class ChatSession { this._apiSettings, this.model, generateContentRequest, + this.chromeAdapter, this.requestOptions ); diff --git a/packages/ai/src/methods/chrome-adapter-browser.test.ts b/packages/ai/src/methods/chrome-adapter-browser.test.ts new file mode 100644 index 00000000000..5d5b2344ab6 --- /dev/null +++ b/packages/ai/src/methods/chrome-adapter-browser.test.ts @@ -0,0 +1,790 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { expect, use } from 'chai'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { chromeAdapterFactory, ChromeAdapterImpl } from './chrome-adapter'; +import { + Availability, + LanguageModel, + LanguageModelCreateOptions, + LanguageModelMessage +} from '../types/language-model'; +import { match, stub } from 'sinon'; +import { GenerateContentRequest, AIErrorCode, InferenceMode } from '../types'; +import { Schema } from '../api'; + +use(sinonChai); +use(chaiAsPromised); + +/** + * Converts the ReadableStream from response.body to an array of strings. + */ +async function toStringArray( + stream: ReadableStream +): Promise { + const decoder = new TextDecoder(); + const actual = []; + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + actual.push(decoder.decode(value)); + } + return actual; +} + +describe('ChromeAdapter', () => { + describe('constructor', () => { + it('sets image as expected input type by default', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const availabilityStub = stub( + languageModelProvider, + 'availability' + ).resolves(Availability.AVAILABLE); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [{ text: 'hi' }] + } + ] + }); + expect(availabilityStub).to.have.been.calledWith({ + expectedInputs: [{ type: 'image' }] + }); + }); + it('honors explicitly set expected inputs', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const availabilityStub = stub( + languageModelProvider, + 'availability' + ).resolves(Availability.AVAILABLE); + const createOptions = { + // Explicitly sets expected inputs. + expectedInputs: [{ type: 'text' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { + createOptions + } + ); + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [{ text: 'hi' }] + } + ] + }); + expect(availabilityStub).to.have.been.calledWith(createOptions); + }); + }); + describe('isAvailable', () => { + it('returns false if mode is only cloud', async () => { + const adapter = new ChromeAdapterImpl( + {} as LanguageModel, + InferenceMode.ONLY_IN_CLOUD + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.false; + }); + it('returns true if mode is only on device and is available', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.AVAILABLE + } as LanguageModel, + InferenceMode.ONLY_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.true; + }); + it('throws if mode is only on device and is unavailable', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.UNAVAILABLE + } as LanguageModel, + InferenceMode.ONLY_ON_DEVICE + ); + await expect( + adapter.isAvailable({ + contents: [] + }) + ).to.be.rejected; + }); + it('returns true after waiting for download if mode is only on device', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.DOWNLOADING, + create: ({}: LanguageModelCreateOptions) => + Promise.resolve({} as LanguageModel) + } as LanguageModel, + InferenceMode.ONLY_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.true; + }); + it('returns false if LanguageModel API is undefined', async () => { + const adapter = new ChromeAdapterImpl( + // @ts-expect-error + undefined, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.false; + }); + it('returns false if request contents empty', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.AVAILABLE + } as LanguageModel, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.false; + }); + it('returns false if request content has "function" role', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.AVAILABLE + } as LanguageModel, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [ + { + role: 'function', + parts: [] + } + ] + }) + ).to.be.false; + }); + it('returns true if request has image with supported mime type', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.AVAILABLE + } as LanguageModel, + InferenceMode.PREFER_ON_DEVICE + ); + for (const mimeType of ChromeAdapterImpl.SUPPORTED_MIME_TYPES) { + expect( + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [ + { + inlineData: { + mimeType, + data: '' + } + } + ] + } + ] + }) + ).to.be.true; + } + }); + it('returns true if model is readily available', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [ + { text: 'describe this image' }, + { inlineData: { mimeType: 'image/jpeg', data: 'asd' } } + ] + } + ] + }) + ).to.be.true; + }); + it('returns false and triggers download when model is available after download', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.DOWNLOADABLE), + create: () => Promise.resolve({}) + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + {} as LanguageModel + ); + const createOptions = { + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + expect( + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }) + ).to.be.false; + expect(createStub).to.have.been.calledOnceWith(createOptions); + }); + it('avoids redundant downloads', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.DOWNLOADABLE), + create: () => Promise.resolve({}) + } as LanguageModel; + const downloadPromise = new Promise(() => { + /* never resolves */ + }); + const createStub = stub(languageModelProvider, 'create').returns( + downloadPromise + ); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }); + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }); + expect(createStub).to.have.been.calledOnce; + }); + it('clears state when download completes', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.DOWNLOADABLE), + create: () => Promise.resolve({}) + } as LanguageModel; + let resolveDownload; + const downloadPromise = new Promise(resolveCallback => { + resolveDownload = resolveCallback; + }); + const createStub = stub(languageModelProvider, 'create').returns( + downloadPromise + ); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }); + resolveDownload!(); + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }); + expect(createStub).to.have.been.calledTwice; + }); + it('returns false when model is never available', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.UNAVAILABLE), + create: () => Promise.resolve({}) + } as LanguageModel; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }) + ).to.be.false; + }); + }); + describe('generateContent', () => { + it('throws if Chrome API is undefined', async () => { + const adapter = new ChromeAdapterImpl( + // @ts-expect-error + undefined, + InferenceMode.ONLY_ON_DEVICE + ); + await expect( + adapter.generateContent({ + contents: [] + }) + ) + .to.eventually.be.rejectedWith( + AIError, + 'Chrome AI requested for unsupported browser version.' + ) + .and.have.property('code', AIErrorCode.UNSUPPORTED); + }); + it('generates content', async () => { + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + prompt: (p: LanguageModelMessage[]) => Promise.resolve('') + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + const promptOutput = 'hi'; + const promptStub = stub(languageModel, 'prompt').resolves(promptOutput); + const createOptions = { + systemPrompt: 'be yourself', + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + const request = { + contents: [{ role: 'user', parts: [{ text: 'anything' }] }] + } as GenerateContentRequest; + const response = await adapter.generateContent(request); + // Asserts initialization params are proxied. + expect(createStub).to.have.been.calledOnceWith(createOptions); + // Asserts Vertex input type is mapped to Chrome type. + expect(promptStub).to.have.been.calledOnceWith([ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ]); + // Asserts expected output. + expect(await response.json()).to.deep.equal({ + candidates: [ + { + content: { + parts: [{ text: promptOutput }] + } + } + ] + }); + }); + it('generates content using image type input', async () => { + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + prompt: (p: LanguageModelMessage[]) => Promise.resolve('') + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + const promptOutput = 'hi'; + const promptStub = stub(languageModel, 'prompt').resolves(promptOutput); + const createOptions = { + systemPrompt: 'be yourself', + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + const request = { + contents: [ + { + role: 'user', + parts: [ + { text: 'anything' }, + { + inlineData: { + data: sampleBase64EncodedImage, + mimeType: 'image/jpeg' + } + } + ] + } + ] + } as GenerateContentRequest; + const response = await adapter.generateContent(request); + // Asserts initialization params are proxied. + expect(createStub).to.have.been.calledOnceWith(createOptions); + // Asserts Vertex input type is mapped to Chrome type. + expect(promptStub).to.have.been.calledOnceWith([ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + }, + { + type: 'image', + value: match.instanceOf(ImageBitmap) + } + ] + } + ]); + // Asserts expected output. + expect(await response.json()).to.deep.equal({ + candidates: [ + { + content: { + parts: [{ text: promptOutput }] + } + } + ] + }); + }); + it('honors prompt options', async () => { + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + prompt: (p: LanguageModelMessage[]) => Promise.resolve('') + } as LanguageModel; + const languageModelProvider = { + create: () => Promise.resolve(languageModel) + } as LanguageModel; + const promptOutput = '{}'; + const promptStub = stub(languageModel, 'prompt').resolves(promptOutput); + const promptOptions = { + responseConstraint: Schema.object({ + properties: {} + }) + }; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { promptOptions } + ); + const request = { + contents: [{ role: 'user', parts: [{ text: 'anything' }] }] + } as GenerateContentRequest; + await adapter.generateContent(request); + expect(promptStub).to.have.been.calledOnceWith( + [ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ], + promptOptions + ); + }); + it('normalizes roles', async () => { + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + prompt: (p: LanguageModelMessage[]) => Promise.resolve('unused') + } as LanguageModel; + const promptStub = stub(languageModel, 'prompt').resolves('unused'); + const languageModelProvider = { + create: () => Promise.resolve(languageModel) + } as LanguageModel; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + const request = { + contents: [{ role: 'model', parts: [{ text: 'unused' }] }] + } as GenerateContentRequest; + await adapter.generateContent(request); + expect(promptStub).to.have.been.calledOnceWith([ + { + // Asserts Vertex's "model" role normalized to Chrome's "assistant" role. + role: 'assistant', + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ]); + }); + }); + describe('countTokens', () => { + it('counts tokens is not yet available', async () => { + const inputText = 'first'; + // setting up stubs + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + measureInputUsage: _i => Promise.resolve(123) + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + + const countTokenRequest = { + contents: [{ role: 'user', parts: [{ text: inputText }] }] + } as GenerateContentRequest; + + try { + await adapter.countTokens(countTokenRequest); + } catch (e) { + // the call to countToken should be rejected with Error + expect((e as AIError).code).to.equal(AIErrorCode.REQUEST_ERROR); + expect((e as AIError).message).includes('not yet available'); + } + + // Asserts that no language model was initialized + expect(createStub).not.called; + }); + }); + describe('generateContentStream', () => { + it('generates content stream', async () => { + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + promptStreaming: _i => new ReadableStream() + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + const part = 'hi'; + const promptStub = stub(languageModel, 'promptStreaming').returns( + new ReadableStream({ + start(controller) { + controller.enqueue([part]); + controller.close(); + } + }) + ); + const createOptions = { + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + const request = { + contents: [{ role: 'user', parts: [{ text: 'anything' }] }] + } as GenerateContentRequest; + const response = await adapter.generateContentStream(request); + expect(createStub).to.have.been.calledOnceWith(createOptions); + expect(promptStub).to.have.been.calledOnceWith([ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ]); + const actual = await toStringArray(response.body!); + expect(actual).to.deep.equal([ + `data: {"candidates":[{"content":{"role":"model","parts":[{"text":["${part}"]}]}}]}\n\n` + ]); + }); + it('generates content stream with image input', async () => { + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + promptStreaming: _i => new ReadableStream() + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + const part = 'hi'; + const promptStub = stub(languageModel, 'promptStreaming').returns( + new ReadableStream({ + start(controller) { + controller.enqueue([part]); + controller.close(); + } + }) + ); + const createOptions = { + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + const request = { + contents: [ + { + role: 'user', + parts: [ + { text: 'anything' }, + { + inlineData: { + data: sampleBase64EncodedImage, + mimeType: 'image/jpeg' + } + } + ] + } + ] + } as GenerateContentRequest; + const response = await adapter.generateContentStream(request); + expect(createStub).to.have.been.calledOnceWith(createOptions); + expect(promptStub).to.have.been.calledOnceWith([ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + }, + { + type: 'image', + value: match.instanceOf(ImageBitmap) + } + ] + } + ]); + const actual = await toStringArray(response.body!); + expect(actual).to.deep.equal([ + `data: {"candidates":[{"content":{"role":"model","parts":[{"text":["${part}"]}]}}]}\n\n` + ]); + }); + it('honors prompt options', async () => { + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + promptStreaming: p => new ReadableStream() + } as LanguageModel; + const languageModelProvider = { + create: () => Promise.resolve(languageModel) + } as LanguageModel; + const promptStub = stub(languageModel, 'promptStreaming').returns( + new ReadableStream() + ); + const promptOptions = { + responseConstraint: Schema.object({ + properties: {} + }) + }; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { promptOptions } + ); + const request = { + contents: [{ role: 'user', parts: [{ text: 'anything' }] }] + } as GenerateContentRequest; + await adapter.generateContentStream(request); + expect(promptStub).to.have.been.calledOnceWith( + [ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ], + promptOptions + ); + }); + it('normalizes roles', async () => { + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + promptStreaming: p => new ReadableStream() + } as LanguageModel; + const promptStub = stub(languageModel, 'promptStreaming').returns( + new ReadableStream() + ); + const languageModelProvider = { + create: () => Promise.resolve(languageModel) + } as LanguageModel; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + const request = { + contents: [{ role: 'model', parts: [{ text: 'unused' }] }] + } as GenerateContentRequest; + await adapter.generateContentStream(request); + expect(promptStub).to.have.been.calledOnceWith([ + { + // Asserts Vertex's "model" role normalized to Chrome's "assistant" role. + role: 'assistant', + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ]); + }); + }); +}); + +describe('chromeAdapterFactory', () => { + it('creates a populated ChromeAdapterImpl', () => { + const fakeLanguageModel = {} as LanguageModel; + const adapter = chromeAdapterFactory( + InferenceMode.PREFER_ON_DEVICE, + { LanguageModel: fakeLanguageModel } as Window, + { createOptions: {} } + ); + expect(adapter?.languageModelProvider).to.equal(fakeLanguageModel); + expect(adapter?.mode).to.equal(InferenceMode.PREFER_ON_DEVICE); + expect(adapter?.onDeviceParams.createOptions).to.exist; + }); +}); + +// TODO: Move to using image from test-utils. +const sampleBase64EncodedImage = + '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'; diff --git a/packages/ai/src/methods/chrome-adapter.ts b/packages/ai/src/methods/chrome-adapter.ts new file mode 100644 index 00000000000..a0ab509e335 --- /dev/null +++ b/packages/ai/src/methods/chrome-adapter.ts @@ -0,0 +1,392 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { logger } from '../logger'; +import { + CountTokensRequest, + GenerateContentRequest, + InferenceMode, + Part, + AIErrorCode, + OnDeviceParams, + Content, + Role +} from '../types'; +import { ChromeAdapter } from '../types/chrome-adapter'; +import { + Availability, + LanguageModel, + LanguageModelMessage, + LanguageModelMessageContent, + LanguageModelMessageRole +} from '../types/language-model'; + +/** + * Defines an inference "backend" that uses Chrome's on-device model, + * and encapsulates logic for detecting when on-device inference is + * possible. + */ +export class ChromeAdapterImpl implements ChromeAdapter { + // Visible for testing + static SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png']; + private isDownloading = false; + private downloadPromise: Promise | undefined; + private oldSession: LanguageModel | undefined; + constructor( + public languageModelProvider: LanguageModel, + public mode: InferenceMode, + public onDeviceParams: OnDeviceParams = { + createOptions: { + // Defaults to support image inputs for convenience. + expectedInputs: [{ type: 'image' }] + } + } + ) {} + + /** + * Checks if a given request can be made on-device. + * + * Encapsulates a few concerns: + * the mode + * API existence + * prompt formatting + * model availability, including triggering download if necessary + * + * + * Pros: callers needn't be concerned with details of on-device availability.

+ * Cons: this method spans a few concerns and splits request validation from usage. + * If instance variables weren't already part of the API, we could consider a better + * separation of concerns. + */ + async isAvailable(request: GenerateContentRequest): Promise { + if (!this.mode) { + logger.debug( + `On-device inference unavailable because mode is undefined.` + ); + return false; + } + if (this.mode === InferenceMode.ONLY_IN_CLOUD) { + logger.debug( + `On-device inference unavailable because mode is "only_in_cloud".` + ); + return false; + } + + // Triggers out-of-band download so model will eventually become available. + const availability = await this.downloadIfAvailable(); + + if (this.mode === InferenceMode.ONLY_ON_DEVICE) { + // If it will never be available due to API inavailability, throw. + if (availability === Availability.UNAVAILABLE) { + throw new AIError( + AIErrorCode.API_NOT_ENABLED, + 'Local LanguageModel API not available in this environment.' + ); + } else if ( + availability === Availability.DOWNLOADABLE || + availability === Availability.DOWNLOADING + ) { + // TODO(chholland): Better user experience during download - progress? + logger.debug(`Waiting for download of LanguageModel to complete.`); + await this.downloadPromise; + return true; + } + return true; + } + + // Applies prefer_on_device logic. + if (availability !== Availability.AVAILABLE) { + logger.debug( + `On-device inference unavailable because availability is "${availability}".` + ); + return false; + } + if (!ChromeAdapterImpl.isOnDeviceRequest(request)) { + logger.debug( + `On-device inference unavailable because request is incompatible.` + ); + return false; + } + + return true; + } + + /** + * Generates content on device. + * + * @remarks + * This is comparable to {@link GenerativeModel.generateContent} for generating content in + * Cloud. + * @param request - a standard Firebase AI {@link GenerateContentRequest} + * @returns {@link Response}, so we can reuse common response formatting. + */ + async generateContent(request: GenerateContentRequest): Promise { + const session = await this.createSession(); + const contents = await Promise.all( + request.contents.map(ChromeAdapterImpl.toLanguageModelMessage) + ); + const text = await session.prompt( + contents, + this.onDeviceParams.promptOptions + ); + return ChromeAdapterImpl.toResponse(text); + } + + /** + * Generates content stream on device. + * + * @remarks + * This is comparable to {@link GenerativeModel.generateContentStream} for generating content in + * Cloud. + * @param request - a standard Firebase AI {@link GenerateContentRequest} + * @returns {@link Response}, so we can reuse common response formatting. + */ + async generateContentStream( + request: GenerateContentRequest + ): Promise { + const session = await this.createSession(); + const contents = await Promise.all( + request.contents.map(ChromeAdapterImpl.toLanguageModelMessage) + ); + const stream = session.promptStreaming( + contents, + this.onDeviceParams.promptOptions + ); + return ChromeAdapterImpl.toStreamResponse(stream); + } + + async countTokens(_request: CountTokensRequest): Promise { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'Count Tokens is not yet available for on-device model.' + ); + } + + /** + * Asserts inference for the given request can be performed by an on-device model. + */ + private static isOnDeviceRequest(request: GenerateContentRequest): boolean { + // Returns false if the prompt is empty. + if (request.contents.length === 0) { + logger.debug('Empty prompt rejected for on-device inference.'); + return false; + } + + for (const content of request.contents) { + if (content.role === 'function') { + logger.debug(`"Function" role rejected for on-device inference.`); + return false; + } + + // Returns false if request contains an image with an unsupported mime type. + for (const part of content.parts) { + if ( + part.inlineData && + ChromeAdapterImpl.SUPPORTED_MIME_TYPES.indexOf( + part.inlineData.mimeType + ) === -1 + ) { + logger.debug( + `Unsupported mime type "${part.inlineData.mimeType}" rejected for on-device inference.` + ); + return false; + } + } + } + + return true; + } + + /** + * Encapsulates logic to get availability and download a model if one is downloadable. + */ + private async downloadIfAvailable(): Promise { + const availability = await this.languageModelProvider?.availability( + this.onDeviceParams.createOptions + ); + + if (availability === Availability.DOWNLOADABLE) { + this.download(); + } + + return availability; + } + + /** + * Triggers out-of-band download of an on-device model. + * + * Chrome only downloads models as needed. Chrome knows a model is needed when code calls + * LanguageModel.create. + * + * Since Chrome manages the download, the SDK can only avoid redundant download requests by + * tracking if a download has previously been requested. + */ + private download(): void { + if (this.isDownloading) { + return; + } + this.isDownloading = true; + this.downloadPromise = this.languageModelProvider + ?.create(this.onDeviceParams.createOptions) + .finally(() => { + this.isDownloading = false; + }); + } + + /** + * Converts Firebase AI {@link Content} object to a Chrome {@link LanguageModelMessage} object. + */ + private static async toLanguageModelMessage( + content: Content + ): Promise { + const languageModelMessageContents = await Promise.all( + content.parts.map(ChromeAdapterImpl.toLanguageModelMessageContent) + ); + return { + role: ChromeAdapterImpl.toLanguageModelMessageRole(content.role), + content: languageModelMessageContents + }; + } + + /** + * Converts a Firebase AI Part object to a Chrome LanguageModelMessageContent object. + */ + private static async toLanguageModelMessageContent( + part: Part + ): Promise { + if (part.text) { + return { + type: 'text', + value: part.text + }; + } else if (part.inlineData) { + const formattedImageContent = await fetch( + `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` + ); + const imageBlob = await formattedImageContent.blob(); + const imageBitmap = await createImageBitmap(imageBlob); + return { + type: 'image', + value: imageBitmap + }; + } + throw new AIError( + AIErrorCode.REQUEST_ERROR, + `Processing of this Part type is not currently supported.` + ); + } + + /** + * Converts a Firebase AI {@link Role} string to a {@link LanguageModelMessageRole} string. + */ + private static toLanguageModelMessageRole( + role: Role + ): LanguageModelMessageRole { + // Assumes 'function' rule has been filtered by isOnDeviceRequest + return role === 'model' ? 'assistant' : 'user'; + } + + /** + * Abstracts Chrome session creation. + * + * Chrome uses a multi-turn session for all inference. Firebase AI uses single-turn for all + * inference. To map the Firebase AI API to Chrome's API, the SDK creates a new session for all + * inference. + * + * Chrome will remove a model from memory if it's no longer in use, so this method ensures a + * new session is created before an old session is destroyed. + */ + private async createSession(): Promise { + if (!this.languageModelProvider) { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'Chrome AI requested for unsupported browser version.' + ); + } + const newSession = await this.languageModelProvider.create( + this.onDeviceParams.createOptions + ); + if (this.oldSession) { + this.oldSession.destroy(); + } + // Holds session reference, so model isn't unloaded from memory. + this.oldSession = newSession; + return newSession; + } + + /** + * Formats string returned by Chrome as a {@link Response} returned by Firebase AI. + */ + private static toResponse(text: string): Response { + return { + json: async () => ({ + candidates: [ + { + content: { + parts: [{ text }] + } + } + ] + }) + } as Response; + } + + /** + * Formats string stream returned by Chrome as SSE returned by Firebase AI. + */ + private static toStreamResponse(stream: ReadableStream): Response { + const encoder = new TextEncoder(); + return { + body: stream.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + const json = JSON.stringify({ + candidates: [ + { + content: { + role: 'model', + parts: [{ text: chunk }] + } + } + ] + }); + controller.enqueue(encoder.encode(`data: ${json}\n\n`)); + } + }) + ) + } as Response; + } +} + +/** + * Creates a ChromeAdapterImpl on demand. + */ +export function chromeAdapterFactory( + mode: InferenceMode, + window?: Window, + params?: OnDeviceParams +): ChromeAdapterImpl | undefined { + // Do not initialize a ChromeAdapter if we are not in hybrid mode. + if (typeof window !== 'undefined' && mode) { + return new ChromeAdapterImpl( + (window as Window).LanguageModel as LanguageModel, + mode, + params + ); + } +} diff --git a/packages/ai/src/methods/count-tokens.test.ts b/packages/ai/src/methods/count-tokens.test.ts index 7e04ddb3561..aabf06a841a 100644 --- a/packages/ai/src/methods/count-tokens.test.ts +++ b/packages/ai/src/methods/count-tokens.test.ts @@ -22,11 +22,12 @@ import chaiAsPromised from 'chai-as-promised'; import { getMockResponse } from '../../test-utils/mock-response'; import * as request from '../requests/request'; import { countTokens } from './count-tokens'; -import { CountTokensRequest } from '../types'; +import { CountTokensRequest, InferenceMode } from '../types'; import { ApiSettings } from '../types/internal'; import { Task } from '../requests/request'; import { mapCountTokensRequest } from '../googleai-mappers'; import { GoogleAIBackend, VertexAIBackend } from '../backend'; +import { ChromeAdapterImpl } from './chrome-adapter'; use(sinonChai); use(chaiAsPromised); @@ -51,6 +52,12 @@ const fakeRequestParams: CountTokensRequest = { contents: [{ parts: [{ text: 'hello' }], role: 'user' }] }; +const fakeChromeAdapter = new ChromeAdapterImpl( + // @ts-expect-error + undefined, + InferenceMode.PREFER_ON_DEVICE +); + describe('countTokens()', () => { afterEach(() => { restore(); @@ -66,7 +73,8 @@ describe('countTokens()', () => { const result = await countTokens( fakeApiSettings, 'model', - fakeRequestParams + fakeRequestParams, + fakeChromeAdapter ); expect(result.totalTokens).to.equal(6); expect(result.totalBillableCharacters).to.equal(16); @@ -92,7 +100,8 @@ describe('countTokens()', () => { const result = await countTokens( fakeApiSettings, 'model', - fakeRequestParams + fakeRequestParams, + fakeChromeAdapter ); expect(result.totalTokens).to.equal(1837); expect(result.totalBillableCharacters).to.equal(117); @@ -120,7 +129,8 @@ describe('countTokens()', () => { const result = await countTokens( fakeApiSettings, 'model', - fakeRequestParams + fakeRequestParams, + fakeChromeAdapter ); expect(result.totalTokens).to.equal(258); expect(result).to.not.have.property('totalBillableCharacters'); @@ -146,7 +156,12 @@ describe('countTokens()', () => { json: mockResponse.json } as Response); await expect( - countTokens(fakeApiSettings, 'model', fakeRequestParams) + countTokens( + fakeApiSettings, + 'model', + fakeRequestParams, + fakeChromeAdapter + ) ).to.be.rejectedWith(/404.*not found/); expect(mockFetch).to.be.called; }); @@ -164,7 +179,12 @@ describe('countTokens()', () => { it('maps request to GoogleAI format', async () => { makeRequestStub.resolves({ ok: true, json: () => {} } as Response); // Unused - await countTokens(fakeGoogleAIApiSettings, 'model', fakeRequestParams); + await countTokens( + fakeGoogleAIApiSettings, + 'model', + fakeRequestParams, + fakeChromeAdapter + ); expect(makeRequestStub).to.be.calledWith( 'model', @@ -176,4 +196,16 @@ describe('countTokens()', () => { ); }); }); + it('throws if mode is ONLY_ON_DEVICE', async () => { + const chromeAdapter = new ChromeAdapterImpl( + // @ts-expect-error + undefined, + InferenceMode.ONLY_ON_DEVICE + ); + await expect( + countTokens(fakeApiSettings, 'model', fakeRequestParams, chromeAdapter) + ).to.be.rejectedWith( + /countTokens\(\) is not supported for on-device models/ + ); + }); }); diff --git a/packages/ai/src/methods/count-tokens.ts b/packages/ai/src/methods/count-tokens.ts index b1e60e3a182..ecd86a82912 100644 --- a/packages/ai/src/methods/count-tokens.ts +++ b/packages/ai/src/methods/count-tokens.ts @@ -15,17 +15,22 @@ * limitations under the License. */ +import { AIError } from '../errors'; import { CountTokensRequest, CountTokensResponse, - RequestOptions + InferenceMode, + RequestOptions, + AIErrorCode } from '../types'; import { Task, makeRequest } from '../requests/request'; import { ApiSettings } from '../types/internal'; import * as GoogleAIMapper from '../googleai-mappers'; import { BackendType } from '../public-types'; +import { ChromeAdapter } from '../types/chrome-adapter'; +import { ChromeAdapterImpl } from './chrome-adapter'; -export async function countTokens( +export async function countTokensOnCloud( apiSettings: ApiSettings, model: string, params: CountTokensRequest, @@ -48,3 +53,21 @@ export async function countTokens( ); return response.json(); } + +export async function countTokens( + apiSettings: ApiSettings, + model: string, + params: CountTokensRequest, + chromeAdapter?: ChromeAdapter, + requestOptions?: RequestOptions +): Promise { + if ( + (chromeAdapter as ChromeAdapterImpl)?.mode === InferenceMode.ONLY_ON_DEVICE + ) { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'countTokens() is not supported for on-device models.' + ); + } + return countTokensOnCloud(apiSettings, model, params, requestOptions); +} diff --git a/packages/ai/src/methods/generate-content.test.ts b/packages/ai/src/methods/generate-content.test.ts index 13250fd83dd..40dc7c7b36e 100644 --- a/packages/ai/src/methods/generate-content.test.ts +++ b/packages/ai/src/methods/generate-content.test.ts @@ -27,17 +27,27 @@ import { GenerateContentRequest, HarmBlockMethod, HarmBlockThreshold, - HarmCategory + HarmCategory, + InferenceMode, + Language, + Outcome } from '../types'; import { ApiSettings } from '../types/internal'; import { Task } from '../requests/request'; import { AIError } from '../api'; import { mapGenerateContentRequest } from '../googleai-mappers'; import { GoogleAIBackend, VertexAIBackend } from '../backend'; +import { ChromeAdapterImpl } from './chrome-adapter'; use(sinonChai); use(chaiAsPromised); +const fakeChromeAdapter = new ChromeAdapterImpl( + // @ts-expect-error + undefined, + InferenceMode.PREFER_ON_DEVICE +); + const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'my-project', @@ -193,6 +203,123 @@ describe('generateContent()', () => { match.any ); }); + it('google search grounding', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-google-search-grounding.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include('The temperature is 67°F (19°C)'); + const groundingMetadata = result.response.candidates?.[0].groundingMetadata; + expect(groundingMetadata).to.not.be.undefined; + expect(groundingMetadata!.searchEntryPoint?.renderedContent).to.contain( + 'div' + ); + expect(groundingMetadata!.groundingChunks?.length).to.equal(2); + expect(groundingMetadata!.groundingChunks?.[0].web?.uri).to.contain( + 'https://vertexaisearch.cloud.google.com' + ); + expect(groundingMetadata!.groundingChunks?.[0].web?.title).to.equal( + 'accuweather.com' + ); + expect(groundingMetadata!.groundingSupports?.length).to.equal(3); + expect( + groundingMetadata!.groundingSupports?.[0].groundingChunkIndices + ).to.deep.equal([0]); + expect(groundingMetadata!.groundingSupports?.[0].segment).to.deep.equal({ + endIndex: 56, + text: 'The current weather in London, United Kingdom is cloudy.' + }); + expect(groundingMetadata!.groundingSupports?.[0].segment?.partIndex).to.be + .undefined; + expect(groundingMetadata!.groundingSupports?.[0].segment?.startIndex).to.be + .undefined; + + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + + it('url context', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-url-context.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include( + 'The temperature is 67°F (19°C)' + ); + const groundingMetadata = + result.response.candidates?.[0].groundingMetadata; + expect(groundingMetadata).to.not.be.undefined; + expect(groundingMetadata!.searchEntryPoint?.renderedContent).to.contain( + 'div' + ); + expect(groundingMetadata!.groundingChunks?.length).to.equal(2); + expect(groundingMetadata!.groundingChunks?.[0].web?.uri).to.contain( + 'https://vertexaisearch.cloud.google.com' + ); + expect(groundingMetadata!.groundingChunks?.[0].web?.title).to.equal( + 'accuweather.com' + ); + expect(groundingMetadata!.groundingSupports?.length).to.equal(3); + expect( + groundingMetadata!.groundingSupports?.[0].groundingChunkIndices + ).to.deep.equal([0]); + expect(groundingMetadata!.groundingSupports?.[0].segment).to.deep.equal({ + endIndex: 56, + text: 'The current weather in London, United Kingdom is cloudy.' + }); + expect(groundingMetadata!.groundingSupports?.[0].segment?.partIndex).to.be + .undefined; + expect(groundingMetadata!.groundingSupports?.[0].segment?.startIndex).to + .be.undefined; + + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + }); + it('codeExecution', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-code-execution.json' + ); + stub(request, 'makeRequest').resolves(mockResponse as Response); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + const parts = result.response.candidates?.[0].content.parts; + expect( + parts?.some(part => part.codeExecutionResult?.outcome === Outcome.OK) + ).to.be.true; + expect( + parts?.some(part => part.executableCode?.language === Language.PYTHON) + ).to.be.true; + }); it('blocked prompt', async () => { const mockResponse = getMockResponse( 'vertexAI', @@ -259,6 +386,22 @@ describe('generateContent()', () => { match.any ); }); + it('empty part', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-empty-part.json' + ); + stub(request, 'makeRequest').resolves(mockResponse as Response); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include( + 'I can certainly help you with that!' + ); + expect(result.response.inlineDataParts()?.length).to.equal(1); + }); it('unknown enum - should ignore', async () => { const mockResponse = getMockResponse( 'vertexAI', @@ -375,4 +518,25 @@ describe('generateContent()', () => { ); }); }); + // TODO: define a similar test for generateContentStream + it('on-device', async () => { + const chromeAdapter = fakeChromeAdapter; + const isAvailableStub = stub(chromeAdapter, 'isAvailable').resolves(true); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ); + const generateContentStub = stub(chromeAdapter, 'generateContent').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams, + chromeAdapter + ); + expect(result.response.text()).to.include('Mountain View, California'); + expect(isAvailableStub).to.be.called; + expect(generateContentStub).to.be.calledWith(fakeRequestParams); + }); }); diff --git a/packages/ai/src/methods/generate-content.ts b/packages/ai/src/methods/generate-content.ts index 5f7902f5954..0e65b479343 100644 --- a/packages/ai/src/methods/generate-content.ts +++ b/packages/ai/src/methods/generate-content.ts @@ -28,17 +28,19 @@ import { processStream } from '../requests/stream-reader'; import { ApiSettings } from '../types/internal'; import * as GoogleAIMapper from '../googleai-mappers'; import { BackendType } from '../public-types'; +import { ChromeAdapter } from '../types/chrome-adapter'; +import { callCloudOrDevice } from '../requests/hybrid-helpers'; -export async function generateContentStream( +async function generateContentStreamOnCloud( apiSettings: ApiSettings, model: string, params: GenerateContentRequest, requestOptions?: RequestOptions -): Promise { +): Promise { if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { params = GoogleAIMapper.mapGenerateContentRequest(params); } - const response = await makeRequest( + return makeRequest( model, Task.STREAM_GENERATE_CONTENT, apiSettings, @@ -46,19 +48,35 @@ export async function generateContentStream( JSON.stringify(params), requestOptions ); +} + +export async function generateContentStream( + apiSettings: ApiSettings, + model: string, + params: GenerateContentRequest, + chromeAdapter?: ChromeAdapter, + requestOptions?: RequestOptions +): Promise { + const response = await callCloudOrDevice( + params, + chromeAdapter, + () => chromeAdapter!.generateContentStream(params), + () => + generateContentStreamOnCloud(apiSettings, model, params, requestOptions) + ); return processStream(response, apiSettings); // TODO: Map streaming responses } -export async function generateContent( +async function generateContentOnCloud( apiSettings: ApiSettings, model: string, params: GenerateContentRequest, requestOptions?: RequestOptions -): Promise { +): Promise { if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { params = GoogleAIMapper.mapGenerateContentRequest(params); } - const response = await makeRequest( + return makeRequest( model, Task.GENERATE_CONTENT, apiSettings, @@ -66,6 +84,21 @@ export async function generateContent( JSON.stringify(params), requestOptions ); +} + +export async function generateContent( + apiSettings: ApiSettings, + model: string, + params: GenerateContentRequest, + chromeAdapter?: ChromeAdapter, + requestOptions?: RequestOptions +): Promise { + const response = await callCloudOrDevice( + params, + chromeAdapter, + () => chromeAdapter!.generateContent(params), + () => generateContentOnCloud(apiSettings, model, params, requestOptions) + ); const generateContentResponse = await processGenerateContentResponse( response, apiSettings diff --git a/packages/ai/src/methods/live-session-helpers.test.ts b/packages/ai/src/methods/live-session-helpers.test.ts new file mode 100644 index 00000000000..cad0475b358 --- /dev/null +++ b/packages/ai/src/methods/live-session-helpers.test.ts @@ -0,0 +1,366 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinon, { SinonFakeTimers, SinonStub, SinonStubbedInstance } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { AIError } from '../errors'; +import { startAudioConversation } from './live-session-helpers'; +import { + FunctionResponse, + LiveServerContent, + LiveServerToolCall +} from '../types'; +import { logger } from '../logger'; +import { isNode } from '@firebase/util'; + +use(sinonChai); +use(chaiAsPromised); + +// A mock message generator to simulate receiving messages from the server. +class MockMessageGenerator { + private resolvers: Array<(result: IteratorResult) => void> = []; + isDone = false; + + next(): Promise> { + return new Promise(resolve => this.resolvers.push(resolve)); + } + + simulateMessage(message: any): void { + const resolver = this.resolvers.shift(); + if (resolver) { + resolver({ value: message, done: false }); + } + } + + endStream(): void { + if (this.isDone) { + return; + } + this.isDone = true; + this.resolvers.forEach(resolve => + resolve({ value: undefined, done: true }) + ); + this.resolvers = []; + } +} + +// A mock LiveSession to intercept calls to the server. +class MockLiveSession { + isClosed = false; + inConversation = false; + send = sinon.stub(); + sendMediaChunks = sinon.stub(); + sendFunctionResponses = sinon.stub(); + messageGenerator = new MockMessageGenerator(); + receive = (): MockMessageGenerator => this.messageGenerator; +} + +// Stubs and mocks for Web APIs used by the helpers. +let mockAudioContext: SinonStubbedInstance; +let mockMediaStream: SinonStubbedInstance; +let getUserMediaStub: SinonStub; +let mockWorkletNode: SinonStubbedInstance; +let mockSourceNode: SinonStubbedInstance; +let mockAudioBufferSource: any; + +function setupGlobalMocks(): void { + // Mock AudioWorkletNode + mockWorkletNode = { + port: { + postMessage: sinon.stub(), + onmessage: null + }, + connect: sinon.stub(), + disconnect: sinon.stub() + } as any; + sinon.stub(global, 'AudioWorkletNode').returns(mockWorkletNode); + + // Mock AudioContext + mockAudioBufferSource = { + connect: sinon.stub(), + start: sinon.stub(), + stop: sinon.stub(), + onended: null, + buffer: { duration: 0.5 } // Mock duration for scheduling + }; + mockSourceNode = { + connect: sinon.stub(), + disconnect: sinon.stub() + } as any; + mockAudioContext = { + resume: sinon.stub().resolves(), + close: sinon.stub().resolves(), + createBuffer: sinon.stub().returns({ + getChannelData: sinon.stub().returns(new Float32Array(1)) + } as any), + createBufferSource: sinon.stub().returns(mockAudioBufferSource), + createMediaStreamSource: sinon.stub().returns(mockSourceNode), + audioWorklet: { + addModule: sinon.stub().resolves() + }, + state: 'suspended' as AudioContextState, + currentTime: 0 + } as any; + sinon.stub(global, 'AudioContext').returns(mockAudioContext); + + // Mock other globals + sinon.stub(global, 'Blob').returns({} as Blob); + sinon.stub(URL, 'createObjectURL').returns('blob:http://localhost/fake-url'); + + // Mock getUserMedia + mockMediaStream = { + getTracks: sinon.stub().returns([{ stop: sinon.stub() } as any]) + } as any; + getUserMediaStub = sinon.stub().resolves(mockMediaStream); + if (typeof navigator === 'undefined') { + (global as any).navigator = { + mediaDevices: { getUserMedia: getUserMediaStub } + }; + } else { + if (!navigator.mediaDevices) { + (navigator as any).mediaDevices = {}; + } + sinon + .stub(navigator.mediaDevices, 'getUserMedia') + .callsFake(getUserMediaStub); + } +} + +describe('Audio Conversation Helpers', () => { + let clock: SinonFakeTimers; + + if (isNode()) { + return; + } + + beforeEach(() => { + clock = sinon.useFakeTimers(); + setupGlobalMocks(); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + describe('startAudioConversation', () => { + let liveSession: MockLiveSession; + beforeEach(() => { + liveSession = new MockLiveSession(); + }); + + it('should throw if the session is closed.', async () => { + liveSession.isClosed = true; + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(AIError, /on a closed LiveSession/); + }); + + it('should throw if a conversation is in progress.', async () => { + liveSession.inConversation = true; + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(AIError, /is already in progress/); + }); + + it('should throw if APIs are not supported.', async () => { + (global as any).AudioWorkletNode = undefined; // Simulate lack of support + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(AIError, /not supported in this environment/); + }); + + it('should throw if microphone permissions are denied.', async () => { + getUserMediaStub.rejects( + new DOMException('Permission denied', 'NotAllowedError') + ); + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(DOMException, /Permission denied/); + }); + + it('should return a controller with a stop method on success.', async () => { + const controller = await startAudioConversation(liveSession as any); + expect(controller).to.have.property('stop').that.is.a('function'); + // Ensure it doesn't throw during cleanup + await expect(controller.stop()).to.be.fulfilled; + }); + }); + + describe('AudioConversationRunner', () => { + let liveSession: MockLiveSession; + let warnStub: SinonStub; + + beforeEach(() => { + liveSession = new MockLiveSession(); + warnStub = sinon.stub(logger, 'warn'); + }); + + afterEach(() => { + warnStub.restore(); + }); + + it('should send processed audio chunks received from the worklet.', async () => { + const controller = await startAudioConversation(liveSession as any); + expect(mockWorkletNode.port.onmessage).to.be.a('function'); + + // Simulate the worklet sending a message + const fakeAudioData = new Int16Array(128); + mockWorkletNode.port.onmessage!({ data: fakeAudioData } as MessageEvent); + + await clock.tickAsync(1); + + expect(liveSession.sendMediaChunks).to.have.been.calledOnce; + const [sentChunk] = liveSession.sendMediaChunks.getCall(0).args[0]; + expect(sentChunk.mimeType).to.equal('audio/pcm'); + expect(sentChunk.data).to.be.a('string'); + await controller.stop(); + }); + + it('should queue and play audio from a serverContent message.', async () => { + const controller = await startAudioConversation(liveSession as any); + const serverMessage: LiveServerContent = { + type: 'serverContent', + modelTurn: { + role: 'model', + parts: [ + { inlineData: { mimeType: 'audio/pcm', data: '1111222233334444' } } + ] // base64 for dummy data + } + }; + + liveSession.messageGenerator.simulateMessage(serverMessage); + await clock.tickAsync(1); // allow message processing + + expect(mockAudioContext.createBuffer).to.have.been.calledOnce; + expect(mockAudioBufferSource.start).to.have.been.calledOnce; + await controller.stop(); + }); + + it('should call function handler and send result on toolCall message.', async () => { + const functionResponse: FunctionResponse = { + id: '1', + name: 'get_weather', + response: { temp: '72F' } + }; + const handlerStub = sinon.stub().resolves(functionResponse); + const controller = await startAudioConversation(liveSession as any, { + functionCallingHandler: handlerStub + }); + + const toolCallMessage: LiveServerToolCall = { + type: 'toolCall', + functionCalls: [ + { id: '1', name: 'get_weather', args: { location: 'LA' } } + ] + }; + + liveSession.messageGenerator.simulateMessage(toolCallMessage); + await clock.tickAsync(1); + + expect(handlerStub).to.have.been.calledOnceWith( + toolCallMessage.functionCalls + ); + expect(liveSession.sendFunctionResponses).to.have.been.calledOnceWith([ + functionResponse + ]); + await controller.stop(); + }); + + it('should clear queue and stop sources on an interruption message.', async () => { + const controller = await startAudioConversation(liveSession as any); + + // 1. Enqueue some audio that is "playing" + const playingMessage: LiveServerContent = { + type: 'serverContent', + modelTurn: { + parts: [ + { inlineData: { mimeType: 'audio/pcm', data: '1111222233334444' } } + ], + role: 'model' + } + }; + liveSession.messageGenerator.simulateMessage(playingMessage); + await clock.tickAsync(1); + expect(mockAudioBufferSource.start).to.have.been.calledOnce; + + // 2. Enqueue another chunk that is now scheduled + liveSession.messageGenerator.simulateMessage(playingMessage); + await clock.tickAsync(1); + expect(mockAudioBufferSource.start).to.have.been.calledTwice; + + // 3. Send interruption message + const interruptionMessage: LiveServerContent = { + type: 'serverContent', + interrupted: true + }; + liveSession.messageGenerator.simulateMessage(interruptionMessage); + await clock.tickAsync(1); + + // Assert that all scheduled sources were stopped. + expect(mockAudioBufferSource.stop).to.have.been.calledTwice; + + // 4. Send new audio post-interruption + const newMessage: LiveServerContent = { + type: 'serverContent', + modelTurn: { + parts: [ + { inlineData: { mimeType: 'audio/pcm', data: '1111222233334444' } } + ], + role: 'model' + } + }; + liveSession.messageGenerator.simulateMessage(newMessage); + await clock.tickAsync(1); + + // Assert a new source was created and started (total of 3 starts) + expect(mockAudioBufferSource.start).to.have.been.calledThrice; + + await controller.stop(); + }); + + it('should warn if no function handler is provided for a toolCall message.', async () => { + const controller = await startAudioConversation(liveSession as any); + liveSession.messageGenerator.simulateMessage({ + type: 'toolCall', + functionCalls: [{ name: 'test' }] + }); + await clock.tickAsync(1); + + expect(warnStub).to.have.been.calledWithMatch( + /functionCallingHandler is undefined/ + ); + await controller.stop(); + }); + + it('stop() should call cleanup and release all resources.', async () => { + const controller = await startAudioConversation(liveSession as any); + + // Need to spy on the internal runner's cleanup method. This is a bit tricky. + // We can't do it directly. Instead, we'll just check the mock results. + await controller.stop(); + + expect(mockWorkletNode.disconnect).to.have.been.calledOnce; + expect(mockSourceNode.disconnect).to.have.been.calledOnce; + expect(mockMediaStream.getTracks()[0].stop).to.have.been.calledOnce; + expect(mockAudioContext.close).to.have.been.calledOnce; + expect(liveSession.inConversation).to.be.false; + }); + }); +}); diff --git a/packages/ai/src/methods/live-session-helpers.ts b/packages/ai/src/methods/live-session-helpers.ts new file mode 100644 index 00000000000..b3907d6219b --- /dev/null +++ b/packages/ai/src/methods/live-session-helpers.ts @@ -0,0 +1,497 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { logger } from '../logger'; +import { + AIErrorCode, + FunctionCall, + FunctionResponse, + GenerativeContentBlob, + LiveServerContent +} from '../types'; +import { LiveSession } from './live-session'; +import { Deferred } from '@firebase/util'; + +const SERVER_INPUT_SAMPLE_RATE = 16_000; +const SERVER_OUTPUT_SAMPLE_RATE = 24_000; + +const AUDIO_PROCESSOR_NAME = 'audio-processor'; + +/** + * The JS for an `AudioWorkletProcessor`. + * This processor is responsible for taking raw audio from the microphone, + * converting it to the required 16-bit 16kHz PCM, and posting it back to the main thread. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor + * + * It is defined as a string here so that it can be converted into a `Blob` + * and loaded at runtime. + */ +const audioProcessorWorkletString = ` + class AudioProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + this.targetSampleRate = options.processorOptions.targetSampleRate; + // 'sampleRate' is a global variable available inside the AudioWorkletGlobalScope, + // representing the native sample rate of the AudioContext. + this.inputSampleRate = sampleRate; + } + + /** + * This method is called by the browser's audio engine for each block of audio data. + * Input is a single input, with a single channel (input[0][0]). + */ + process(inputs) { + const input = inputs[0]; + if (input && input.length > 0 && input[0].length > 0) { + const pcmData = input[0]; // Float32Array of raw audio samples. + + // Simple linear interpolation for resampling. + const resampled = new Float32Array(Math.round(pcmData.length * this.targetSampleRate / this.inputSampleRate)); + const ratio = pcmData.length / resampled.length; + for (let i = 0; i < resampled.length; i++) { + resampled[i] = pcmData[Math.floor(i * ratio)]; + } + + // Convert Float32 (-1, 1) samples to Int16 (-32768, 32767) + const resampledInt16 = new Int16Array(resampled.length); + for (let i = 0; i < resampled.length; i++) { + const sample = Math.max(-1, Math.min(1, resampled[i])); + if (sample < 0) { + resampledInt16[i] = sample * 32768; + } else { + resampledInt16[i] = sample * 32767; + } + } + + this.port.postMessage(resampledInt16); + } + // Return true to keep the processor alive and processing the next audio block. + return true; + } + } + + // Register the processor with a name that can be used to instantiate it from the main thread. + registerProcessor('${AUDIO_PROCESSOR_NAME}', AudioProcessor); +`; + +/** + * A controller for managing an active audio conversation. + * + * @beta + */ +export interface AudioConversationController { + /** + * Stops the audio conversation, closes the microphone connection, and + * cleans up resources. Returns a promise that resolves when cleanup is complete. + */ + stop: () => Promise; +} + +/** + * Options for {@link startAudioConversation}. + * + * @beta + */ +export interface StartAudioConversationOptions { + /** + * An async handler that is called when the model requests a function to be executed. + * The handler should perform the function call and return the result as a `Part`, + * which will then be sent back to the model. + */ + functionCallingHandler?: ( + functionCalls: FunctionCall[] + ) => Promise; +} + +/** + * Dependencies needed by the {@link AudioConversationRunner}. + * + * @internal + */ +interface RunnerDependencies { + audioContext: AudioContext; + mediaStream: MediaStream; + sourceNode: MediaStreamAudioSourceNode; + workletNode: AudioWorkletNode; +} + +/** + * Encapsulates the core logic of an audio conversation. + * + * @internal + */ +export class AudioConversationRunner { + /** A flag to indicate if the conversation has been stopped. */ + private isStopped = false; + /** A deferred that contains a promise that is resolved when stop() is called, to unblock the receive loop. */ + private readonly stopDeferred = new Deferred(); + /** A promise that tracks the lifecycle of the main `runReceiveLoop`. */ + private readonly receiveLoopPromise: Promise; + + /** A FIFO queue of 24kHz, 16-bit PCM audio chunks received from the server. */ + private readonly playbackQueue: ArrayBuffer[] = []; + /** Tracks scheduled audio sources. Used to cancel scheduled audio when the model is interrupted. */ + private scheduledSources: AudioBufferSourceNode[] = []; + /** A high-precision timeline pointer for scheduling gapless audio playback. */ + private nextStartTime = 0; + /** A mutex to prevent the playback processing loop from running multiple times concurrently. */ + private isPlaybackLoopRunning = false; + + constructor( + private readonly liveSession: LiveSession, + private readonly options: StartAudioConversationOptions, + private readonly deps: RunnerDependencies + ) { + this.liveSession.inConversation = true; + + // Start listening for messages from the server. + this.receiveLoopPromise = this.runReceiveLoop().finally(() => + this.cleanup() + ); + + // Set up the handler for receiving processed audio data from the worklet. + // Message data has been resampled to 16kHz 16-bit PCM. + this.deps.workletNode.port.onmessage = event => { + if (this.isStopped) { + return; + } + + const pcm16 = event.data as Int16Array; + const base64 = btoa( + String.fromCharCode.apply( + null, + Array.from(new Uint8Array(pcm16.buffer)) + ) + ); + + const chunk: GenerativeContentBlob = { + mimeType: 'audio/pcm', + data: base64 + }; + void this.liveSession.sendMediaChunks([chunk]); + }; + } + + /** + * Stops the conversation and unblocks the main receive loop. + */ + async stop(): Promise { + if (this.isStopped) { + return; + } + this.isStopped = true; + this.stopDeferred.resolve(); // Unblock the receive loop + await this.receiveLoopPromise; // Wait for the loop and cleanup to finish + } + + /** + * Cleans up all audio resources (nodes, stream tracks, context) and marks the + * session as no longer in a conversation. + */ + private cleanup(): void { + this.interruptPlayback(); // Ensure all audio is stopped on final cleanup. + this.deps.workletNode.port.onmessage = null; + this.deps.workletNode.disconnect(); + this.deps.sourceNode.disconnect(); + this.deps.mediaStream.getTracks().forEach(track => track.stop()); + if (this.deps.audioContext.state !== 'closed') { + void this.deps.audioContext.close(); + } + this.liveSession.inConversation = false; + } + + /** + * Adds audio data to the queue and ensures the playback loop is running. + */ + private enqueueAndPlay(audioData: ArrayBuffer): void { + this.playbackQueue.push(audioData); + // Will no-op if it's already running. + void this.processPlaybackQueue(); + } + + /** + * Stops all current and pending audio playback and clears the queue. This is + * called when the server indicates the model's speech was interrupted with + * `LiveServerContent.modelTurn.interrupted`. + */ + private interruptPlayback(): void { + // Stop all sources that have been scheduled. The onended event will fire for each, + // which will clean up the scheduledSources array. + [...this.scheduledSources].forEach(source => source.stop(0)); + + // Clear the internal buffer of unprocessed audio chunks. + this.playbackQueue.length = 0; + + // Reset the playback clock to start fresh. + this.nextStartTime = this.deps.audioContext.currentTime; + } + + /** + * Processes the playback queue in a loop, scheduling each chunk in a gapless sequence. + */ + private async processPlaybackQueue(): Promise { + if (this.isPlaybackLoopRunning) { + return; + } + this.isPlaybackLoopRunning = true; + + while (this.playbackQueue.length > 0 && !this.isStopped) { + const pcmRawBuffer = this.playbackQueue.shift()!; + try { + const pcm16 = new Int16Array(pcmRawBuffer); + const frameCount = pcm16.length; + + const audioBuffer = this.deps.audioContext.createBuffer( + 1, + frameCount, + SERVER_OUTPUT_SAMPLE_RATE + ); + + // Convert 16-bit PCM to 32-bit PCM, required by the Web Audio API. + const channelData = audioBuffer.getChannelData(0); + for (let i = 0; i < frameCount; i++) { + channelData[i] = pcm16[i] / 32768; // Normalize to Float32 range [-1.0, 1.0] + } + + const source = this.deps.audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(this.deps.audioContext.destination); + + // Track the source and set up a handler to remove it from tracking when it finishes. + this.scheduledSources.push(source); + source.onended = () => { + this.scheduledSources = this.scheduledSources.filter( + s => s !== source + ); + }; + + // To prevent gaps, schedule the next chunk to start either now (if we're catching up) + // or exactly when the previous chunk is scheduled to end. + this.nextStartTime = Math.max( + this.deps.audioContext.currentTime, + this.nextStartTime + ); + source.start(this.nextStartTime); + + // Update the schedule for the *next* chunk. + this.nextStartTime += audioBuffer.duration; + } catch (e) { + logger.error('Error playing audio:', e); + } + } + + this.isPlaybackLoopRunning = false; + } + + /** + * The main loop that listens for and processes messages from the server. + */ + private async runReceiveLoop(): Promise { + const messageGenerator = this.liveSession.receive(); + while (!this.isStopped) { + const result = await Promise.race([ + messageGenerator.next(), + this.stopDeferred.promise + ]); + + if (this.isStopped || !result || result.done) { + break; + } + + const message = result.value; + if (message.type === 'serverContent') { + const serverContent = message as LiveServerContent; + if (serverContent.interrupted) { + this.interruptPlayback(); + } + + const audioPart = serverContent.modelTurn?.parts.find(part => + part.inlineData?.mimeType.startsWith('audio/') + ); + if (audioPart?.inlineData) { + const audioData = Uint8Array.from( + atob(audioPart.inlineData.data), + c => c.charCodeAt(0) + ).buffer; + this.enqueueAndPlay(audioData); + } + } else if (message.type === 'toolCall') { + if (!this.options.functionCallingHandler) { + logger.warn( + 'Received tool call message, but StartAudioConversationOptions.functionCallingHandler is undefined. Ignoring tool call.' + ); + } else { + try { + const functionResponse = await this.options.functionCallingHandler( + message.functionCalls + ); + if (!this.isStopped) { + void this.liveSession.sendFunctionResponses([functionResponse]); + } + } catch (e) { + throw new AIError( + AIErrorCode.ERROR, + `Function calling handler failed: ${(e as Error).message}` + ); + } + } + } + } + } +} + +/** + * Starts a real-time, bidirectional audio conversation with the model. This helper function manages + * the complexities of microphone access, audio recording, playback, and interruptions. + * + * @remarks Important: This function must be called in response to a user gesture + * (for example, a button click) to comply with {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy | browser autoplay policies}. + * + * @example + * ```javascript + * const liveSession = await model.connect(); + * let conversationController; + * + * // This function must be called from within a click handler. + * async function startConversation() { + * try { + * conversationController = await startAudioConversation(liveSession); + * } catch (e) { + * // Handle AI-specific errors + * if (e instanceof AIError) { + * console.error("AI Error:", e.message); + * } + * // Handle microphone permission and hardware errors + * else if (e instanceof DOMException) { + * console.error("Microphone Error:", e.message); + * } + * // Handle other unexpected errors + * else { + * console.error("An unexpected error occurred:", e); + * } + * } + * } + * + * // Later, to stop the conversation: + * // if (conversationController) { + * // await conversationController.stop(); + * // } + * ``` + * + * @param liveSession - An active {@link LiveSession} instance. + * @param options - Configuration options for the audio conversation. + * @returns A `Promise` that resolves with an {@link AudioConversationController}. + * @throws `AIError` if the environment does not support required Web APIs (`UNSUPPORTED`), if a conversation is already active (`REQUEST_ERROR`), the session is closed (`SESSION_CLOSED`), or if an unexpected initialization error occurs (`ERROR`). + * @throws `DOMException` Thrown by `navigator.mediaDevices.getUserMedia()` if issues occur with microphone access, such as permissions being denied (`NotAllowedError`) or no compatible hardware being found (`NotFoundError`). See the {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#exceptions | MDN documentation} for a full list of exceptions. + * + * @beta + */ +export async function startAudioConversation( + liveSession: LiveSession, + options: StartAudioConversationOptions = {} +): Promise { + if (liveSession.isClosed) { + throw new AIError( + AIErrorCode.SESSION_CLOSED, + 'Cannot start audio conversation on a closed LiveSession.' + ); + } + + if (liveSession.inConversation) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'An audio conversation is already in progress for this session.' + ); + } + + // Check for necessary Web API support. + if ( + typeof AudioWorkletNode === 'undefined' || + typeof AudioContext === 'undefined' || + typeof navigator === 'undefined' || + !navigator.mediaDevices + ) { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'Audio conversation is not supported in this environment. It requires the Web Audio API and AudioWorklet support.' + ); + } + + let audioContext: AudioContext | undefined; + try { + // 1. Set up the audio context. This must be in response to a user gesture. + // See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy + audioContext = new AudioContext(); + if (audioContext.state === 'suspended') { + await audioContext.resume(); + } + + // 2. Prompt for microphone access and get the media stream. + // This can throw a variety of permission or hardware-related errors. + const mediaStream = await navigator.mediaDevices.getUserMedia({ + audio: true + }); + + // 3. Load the AudioWorklet processor. + // See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet + const workletBlob = new Blob([audioProcessorWorkletString], { + type: 'application/javascript' + }); + const workletURL = URL.createObjectURL(workletBlob); + await audioContext.audioWorklet.addModule(workletURL); + + // 4. Create the audio graph: Microphone -> Source Node -> Worklet Node + const sourceNode = audioContext.createMediaStreamSource(mediaStream); + const workletNode = new AudioWorkletNode( + audioContext, + AUDIO_PROCESSOR_NAME, + { + processorOptions: { targetSampleRate: SERVER_INPUT_SAMPLE_RATE } + } + ); + sourceNode.connect(workletNode); + + // 5. Instantiate and return the runner which manages the conversation. + const runner = new AudioConversationRunner(liveSession, options, { + audioContext, + mediaStream, + sourceNode, + workletNode + }); + + return { stop: () => runner.stop() }; + } catch (e) { + // Ensure the audio context is closed on any setup error. + if (audioContext && audioContext.state !== 'closed') { + void audioContext.close(); + } + + // Re-throw specific, known error types directly. The user may want to handle `DOMException` + // errors differently (for example, if permission to access audio device was denied). + if (e instanceof AIError || e instanceof DOMException) { + throw e; + } + + // Wrap any other unexpected errors in a standard AIError. + throw new AIError( + AIErrorCode.ERROR, + `Failed to initialize audio recording: ${(e as Error).message}` + ); + } +} diff --git a/packages/ai/src/methods/live-session.test.ts b/packages/ai/src/methods/live-session.test.ts new file mode 100644 index 00000000000..7454b1208c9 --- /dev/null +++ b/packages/ai/src/methods/live-session.test.ts @@ -0,0 +1,324 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { + FunctionResponse, + LiveResponseType, + LiveServerContent, + LiveServerToolCall, + LiveServerToolCallCancellation +} from '../types'; +import { LiveSession } from './live-session'; +import { WebSocketHandler } from '../websocket'; +import { AIError } from '../errors'; +import { logger } from '../logger'; + +use(sinonChai); +use(chaiAsPromised); + +class MockWebSocketHandler implements WebSocketHandler { + connect = stub().resolves(); + send = spy(); + close = stub().resolves(); + + private messageQueue: unknown[] = []; + private streamClosed = false; + private listenerPromiseResolver: (() => void) | null = null; + + async *listen(): AsyncGenerator { + while (!this.streamClosed) { + if (this.messageQueue.length > 0) { + yield this.messageQueue.shift(); + } else { + // Wait until a new message is pushed or the stream is ended. + await new Promise(resolve => { + this.listenerPromiseResolver = resolve; + }); + } + } + } + + simulateServerMessage(message: object): void { + this.messageQueue.push(message); + if (this.listenerPromiseResolver) { + // listener is waiting for our message + this.listenerPromiseResolver(); + this.listenerPromiseResolver = null; + } + } + + endStream(): void { + this.streamClosed = true; + if (this.listenerPromiseResolver) { + this.listenerPromiseResolver(); + this.listenerPromiseResolver = null; + } + } +} + +describe('LiveSession', () => { + let mockHandler: MockWebSocketHandler; + let session: LiveSession; + let serverMessagesGenerator: AsyncGenerator; + + beforeEach(() => { + mockHandler = new MockWebSocketHandler(); + serverMessagesGenerator = mockHandler.listen(); + session = new LiveSession(mockHandler, serverMessagesGenerator); + }); + + describe('send()', () => { + it('should format and send a valid text message', async () => { + await session.send('Hello there'); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + clientContent: { + turns: [{ role: 'user', parts: [{ text: 'Hello there' }] }], + turnComplete: true + } + }); + }); + + it('should format and send a message with an array of Parts', async () => { + const parts = [ + { text: 'Part 1' }, + { inlineData: { mimeType: 'image/png', data: 'base64==' } } + ]; + await session.send(parts); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData.clientContent.turns[0].parts).to.deep.equal(parts); + }); + }); + + describe('sendMediaChunks()', () => { + it('should send a correctly formatted realtimeInput message', async () => { + const chunks = [{ data: 'base64', mimeType: 'audio/webm' }]; + await session.sendMediaChunks(chunks); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + realtimeInput: { mediaChunks: chunks } + }); + }); + }); + + describe('sendMediaStream()', () => { + it('should send multiple chunks from a stream', async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue({ data: 'chunk1', mimeType: 'audio/webm' }); + controller.enqueue({ data: 'chunk2', mimeType: 'audio/webm' }); + controller.close(); + } + }); + + await session.sendMediaStream(stream); + + expect(mockHandler.send).to.have.been.calledTwice; + const firstCall = JSON.parse(mockHandler.send.getCall(0).args[0]); + const secondCall = JSON.parse(mockHandler.send.getCall(1).args[0]); + expect(firstCall.realtimeInput.mediaChunks[0].data).to.equal('chunk1'); + expect(secondCall.realtimeInput.mediaChunks[0].data).to.equal('chunk2'); + }); + + it('should re-throw an AIError if the stream reader throws', async () => { + const errorStream = new ReadableStream({ + pull(controller) { + controller.error(new Error('Stream failed!')); + } + }); + await expect(session.sendMediaStream(errorStream)).to.be.rejectedWith( + AIError, + /Stream failed!/ + ); + }); + }); + + describe('sendFunctionResponses()', () => { + it('should send all function responses', async () => { + const functionResponses: FunctionResponse[] = [ + { + id: 'function-call-1', + name: 'function-name', + response: { + result: 'foo' + } + }, + { + id: 'function-call-2', + name: 'function-name-2', + response: { + result: 'bar' + } + } + ]; + await session.sendFunctionResponses(functionResponses); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + toolResponse: { + functionResponses + } + }); + }); + }); + + describe('receive()', () => { + it('should correctly parse and transform all server message types', async () => { + const receivePromise = (async () => { + const responses = []; + for await (const response of session.receive()) { + responses.push(response); + } + return responses; + })(); + + mockHandler.simulateServerMessage({ + serverContent: { modelTurn: { parts: [{ text: 'response 1' }] } } + }); + mockHandler.simulateServerMessage({ + toolCall: { functionCalls: [{ name: 'test_func' }] } + }); + mockHandler.simulateServerMessage({ + toolCallCancellation: { functionIds: ['123'] } + }); + mockHandler.simulateServerMessage({ + serverContent: { turnComplete: true } + }); + await new Promise(r => setTimeout(() => r(), 10)); // Wait for the listener to process messages + mockHandler.endStream(); + + const responses = await receivePromise; + expect(responses).to.have.lengthOf(4); + expect(responses[0]).to.deep.equal({ + type: LiveResponseType.SERVER_CONTENT, + modelTurn: { parts: [{ text: 'response 1' }] } + } as LiveServerContent); + expect(responses[1]).to.deep.equal({ + type: LiveResponseType.TOOL_CALL, + functionCalls: [{ name: 'test_func' }] + } as LiveServerToolCall); + expect(responses[2]).to.deep.equal({ + type: LiveResponseType.TOOL_CALL_CANCELLATION, + functionIds: ['123'] + } as LiveServerToolCallCancellation); + }); + + it('should log a warning and skip messages that are not objects', async () => { + const loggerStub = stub(logger, 'warn'); + const receivePromise = (async () => { + const responses = []; + for await (const response of session.receive()) { + responses.push(response); + } + return responses; + })(); + + mockHandler.simulateServerMessage(null as any); + mockHandler.simulateServerMessage('not an object' as any); + await new Promise(r => setTimeout(() => r(), 10)); // Wait for the listener to process messages + mockHandler.endStream(); + + const responses = await receivePromise; + expect(responses).to.be.empty; + expect(loggerStub).to.have.been.calledTwice; + expect(loggerStub).to.have.been.calledWithMatch( + /Received an invalid message/ + ); + + loggerStub.restore(); + }); + + it('should log a warning and skip objects of unknown type', async () => { + const loggerStub = stub(logger, 'warn'); + const receivePromise = (async () => { + const responses = []; + for await (const response of session.receive()) { + responses.push(response); + } + return responses; + })(); + + mockHandler.simulateServerMessage({ unknownType: { data: 'test' } }); + await new Promise(r => setTimeout(() => r(), 10)); // Wait for the listener to process messages + mockHandler.endStream(); + + const responses = await receivePromise; + expect(responses).to.be.empty; + expect(loggerStub).to.have.been.calledOnce; + expect(loggerStub).to.have.been.calledWithMatch( + /Received an unknown message type/ + ); + + loggerStub.restore(); + }); + }); + + describe('close()', () => { + it('should call the handler, set the isClosed flag, and be idempotent', async () => { + expect(session.isClosed).to.be.false; + await session.close(); + expect(mockHandler.close).to.have.been.calledOnce; + expect(session.isClosed).to.be.true; + + // Call again to test idempotency + await session.close(); + expect(mockHandler.close).to.have.been.calledOnce; // Should not be called again + }); + + it('should terminate an active receive() loop', async () => { + const received: unknown[] = []; + const receivePromise = (async () => { + for await (const msg of session.receive()) { + received.push(msg); + } + })(); + + mockHandler.simulateServerMessage({ + serverContent: { modelTurn: { parts: [{ text: 'one' }] } } + }); + // Allow the first message to be processed + await new Promise(r => setTimeout(r, 10)); + expect(received).to.have.lengthOf(1); + + await session.close(); + mockHandler.endStream(); // End the mock stream + + await receivePromise; // This should now resolve + + // No more messages should have been processed + expect(received).to.have.lengthOf(1); + }); + + it('methods should throw after session is closed', async () => { + await session.close(); + await expect(session.send('test')).to.be.rejectedWith(AIError, /closed/); + await expect(session.sendMediaChunks([])).to.be.rejectedWith( + AIError, + /closed/ + ); + const generator = session.receive(); + await expect(generator.next()).to.be.rejectedWith(AIError, /closed/); + }); + }); +}); diff --git a/packages/ai/src/methods/live-session.ts b/packages/ai/src/methods/live-session.ts new file mode 100644 index 00000000000..92d325e2f0d --- /dev/null +++ b/packages/ai/src/methods/live-session.ts @@ -0,0 +1,262 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AIErrorCode, + FunctionResponse, + GenerativeContentBlob, + LiveResponseType, + LiveServerContent, + LiveServerToolCall, + LiveServerToolCallCancellation, + Part +} from '../public-types'; +import { formatNewContent } from '../requests/request-helpers'; +import { AIError } from '../errors'; +import { WebSocketHandler } from '../websocket'; +import { logger } from '../logger'; +import { + _LiveClientContent, + _LiveClientRealtimeInput, + _LiveClientToolResponse +} from '../types/live-responses'; + +/** + * Represents an active, real-time, bidirectional conversation with the model. + * + * This class should only be instantiated by calling {@link LiveGenerativeModel.connect}. + * + * @beta + */ +export class LiveSession { + /** + * Indicates whether this Live session is closed. + * + * @beta + */ + isClosed = false; + /** + * Indicates whether this Live session is being controlled by an `AudioConversationController`. + * + * @beta + */ + inConversation = false; + + /** + * @internal + */ + constructor( + private webSocketHandler: WebSocketHandler, + private serverMessages: AsyncGenerator + ) {} + + /** + * Sends content to the server. + * + * @param request - The message to send to the model. + * @param turnComplete - Indicates if the turn is complete. Defaults to false. + * @throws If this session has been closed. + * + * @beta + */ + async send( + request: string | Array, + turnComplete = true + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const newContent = formatNewContent(request); + + const message: _LiveClientContent = { + clientContent: { + turns: [newContent], + turnComplete + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + + /** + * Sends realtime input to the server. + * + * @param mediaChunks - The media chunks to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + // The backend does not support sending more than one mediaChunk in one message. + // Work around this limitation by sending mediaChunks in separate messages. + mediaChunks.forEach(mediaChunk => { + const message: _LiveClientRealtimeInput = { + realtimeInput: { mediaChunks: [mediaChunk] } + }; + this.webSocketHandler.send(JSON.stringify(message)); + }); + } + + /** + * Sends function responses to the server. + * + * @param functionResponses - The function responses to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendFunctionResponses( + functionResponses: FunctionResponse[] + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const message: _LiveClientToolResponse = { + toolResponse: { + functionResponses + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + + /** + * Sends a stream of {@link GenerativeContentBlob}. + * + * @param mediaChunkStream - The stream of {@link GenerativeContentBlob} to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaStream( + mediaChunkStream: ReadableStream + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const reader = mediaChunkStream.getReader(); + while (true) { + try { + const { done, value } = await reader.read(); + + if (done) { + break; + } else if (!value) { + throw new Error('Missing chunk in reader, but reader is not done.'); + } + + await this.sendMediaChunks([value]); + } catch (e) { + // Re-throw any errors that occur during stream consumption or sending. + const message = + e instanceof Error ? e.message : 'Error processing media stream.'; + throw new AIError(AIErrorCode.REQUEST_ERROR, message); + } + } + } + + /** + * Yields messages received from the server. + * This can only be used by one consumer at a time. + * + * @returns An `AsyncGenerator` that yields server messages as they arrive. + * @throws If the session is already closed, or if we receive a response that we don't support. + * + * @beta + */ + async *receive(): AsyncGenerator< + LiveServerContent | LiveServerToolCall | LiveServerToolCallCancellation + > { + if (this.isClosed) { + throw new AIError( + AIErrorCode.SESSION_CLOSED, + 'Cannot read from a Live session that is closed. Try starting a new Live session.' + ); + } + for await (const message of this.serverMessages) { + if (message && typeof message === 'object') { + if (LiveResponseType.SERVER_CONTENT in message) { + yield { + type: 'serverContent', + ...(message as { serverContent: Omit }) + .serverContent + } as LiveServerContent; + } else if (LiveResponseType.TOOL_CALL in message) { + yield { + type: 'toolCall', + ...(message as { toolCall: Omit }) + .toolCall + } as LiveServerToolCall; + } else if (LiveResponseType.TOOL_CALL_CANCELLATION in message) { + yield { + type: 'toolCallCancellation', + ...( + message as { + toolCallCancellation: Omit< + LiveServerToolCallCancellation, + 'type' + >; + } + ).toolCallCancellation + } as LiveServerToolCallCancellation; + } else { + logger.warn( + `Received an unknown message type from the server: ${JSON.stringify( + message + )}` + ); + } + } else { + logger.warn( + `Received an invalid message from the server: ${JSON.stringify( + message + )}` + ); + } + } + } + + /** + * Closes this session. + * All methods on this session will throw an error once this resolves. + * + * @beta + */ + async close(): Promise { + if (!this.isClosed) { + this.isClosed = true; + await this.webSocketHandler.close(1000, 'Client closed session.'); + } + } +} diff --git a/packages/ai/src/models/ai-model.test.ts b/packages/ai/src/models/ai-model.test.ts index 4f23fe9d06f..2e8f8998c58 100644 --- a/packages/ai/src/models/ai-model.test.ts +++ b/packages/ai/src/models/ai-model.test.ts @@ -17,9 +17,11 @@ import { use, expect } from 'chai'; import { AI, AIErrorCode } from '../public-types'; import sinonChai from 'sinon-chai'; +import { stub } from 'sinon'; import { AIModel } from './ai-model'; import { AIError } from '../errors'; import { VertexAIBackend } from '../backend'; +import { AIService } from '../service'; use(sinonChai); @@ -67,6 +69,52 @@ describe('AIModel', () => { const testModel = new TestModel(fakeAI, 'tunedModels/my-model'); expect(testModel.model).to.equal('tunedModels/my-model'); }); + it('calls regular app check token when option is set', async () => { + const getTokenStub = stub().resolves(); + const getLimitedUseTokenStub = stub().resolves(); + const testModel = new TestModel( + //@ts-ignore + { + ...fakeAI, + options: { useLimitedUseAppCheckTokens: false }, + appCheck: { + getToken: getTokenStub, + getLimitedUseToken: getLimitedUseTokenStub + } + } as AIService, + 'models/my-model' + ); + if (testModel._apiSettings?.getAppCheckToken) { + await testModel._apiSettings.getAppCheckToken(); + } + expect(getTokenStub).to.be.called; + expect(getLimitedUseTokenStub).to.not.be.called; + getTokenStub.reset(); + getLimitedUseTokenStub.reset(); + }); + it('calls limited use token when option is set', async () => { + const getTokenStub = stub().resolves(); + const getLimitedUseTokenStub = stub().resolves(); + const testModel = new TestModel( + //@ts-ignore + { + ...fakeAI, + options: { useLimitedUseAppCheckTokens: true }, + appCheck: { + getToken: getTokenStub, + getLimitedUseToken: getLimitedUseTokenStub + } + } as AIService, + 'models/my-model' + ); + if (testModel._apiSettings?.getAppCheckToken) { + await testModel._apiSettings.getAppCheckToken(); + } + expect(getTokenStub).to.not.be.called; + expect(getLimitedUseTokenStub).to.be.called; + getTokenStub.reset(); + getLimitedUseTokenStub.reset(); + }); it('throws if not passed an api key', () => { const fakeAI: AI = { app: { diff --git a/packages/ai/src/models/ai-model.ts b/packages/ai/src/models/ai-model.ts index 084dbe329cc..3fe202d5eb2 100644 --- a/packages/ai/src/models/ai-model.ts +++ b/packages/ai/src/models/ai-model.ts @@ -39,7 +39,7 @@ export abstract class AIModel { /** * @internal */ - protected _apiSettings: ApiSettings; + _apiSettings: ApiSettings; /** * Constructs a new instance of the {@link AIModel} class. @@ -90,8 +90,13 @@ export abstract class AIModel { return Promise.resolve({ token }); }; } else if ((ai as AIService).appCheck) { - this._apiSettings.getAppCheckToken = () => - (ai as AIService).appCheck!.getToken(); + if (ai.options?.useLimitedUseAppCheckTokens) { + this._apiSettings.getAppCheckToken = () => + (ai as AIService).appCheck!.getLimitedUseToken(); + } else { + this._apiSettings.getAppCheckToken = () => + (ai as AIService).appCheck!.getToken(); + } } if ((ai as AIService).auth) { diff --git a/packages/ai/src/models/generative-model.test.ts b/packages/ai/src/models/generative-model.test.ts index d055b82b1be..bcd78d746d4 100644 --- a/packages/ai/src/models/generative-model.test.ts +++ b/packages/ai/src/models/generative-model.test.ts @@ -16,14 +16,26 @@ */ import { use, expect } from 'chai'; import { GenerativeModel } from './generative-model'; -import { FunctionCallingMode, AI } from '../public-types'; +import { + FunctionCallingMode, + AI, + InferenceMode, + AIErrorCode +} from '../public-types'; import * as request from '../requests/request'; -import { match, restore, stub } from 'sinon'; -import { getMockResponse } from '../../test-utils/mock-response'; +import { SinonStub, match, restore, stub } from 'sinon'; +import { + getMockResponse, + getMockResponseStreaming +} from '../../test-utils/mock-response'; import sinonChai from 'sinon-chai'; import { VertexAIBackend } from '../backend'; +import { ChromeAdapterImpl } from '../methods/chrome-adapter'; +import { AIError } from '../errors'; +import chaiAsPromised from 'chai-as-promised'; use(sinonChai); +use(chaiAsPromised); const fakeAI: AI = { app: { @@ -39,24 +51,39 @@ const fakeAI: AI = { location: 'us-central1' }; +const fakeChromeAdapter = new ChromeAdapterImpl( + // @ts-expect-error + undefined, + InferenceMode.PREFER_ON_DEVICE +); + describe('GenerativeModel', () => { it('passes params through to generateContent', async () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - tools: [ - { - functionDeclarations: [ - { - name: 'myfunc', - description: 'mydesc' - } - ] - } - ], - toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, - systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } - }); - expect(genModel.tools?.length).to.equal(1); + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + tools: [ + { + functionDeclarations: [ + { + name: 'myfunc', + description: 'mydesc' + } + ] + }, + { googleSearch: {} }, + { codeExecution: {} } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.NONE } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + }, + {}, + fakeChromeAdapter + ); + expect(genModel.tools?.length).to.equal(3); expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( FunctionCallingMode.NONE ); @@ -77,6 +104,8 @@ describe('GenerativeModel', () => { match((value: string) => { return ( value.includes('myfunc') && + value.includes('googleSearch') && + value.includes('codeExecution') && value.includes(FunctionCallingMode.NONE) && value.includes('be friendly') ); @@ -86,10 +115,15 @@ describe('GenerativeModel', () => { restore(); }); it('passes text-only systemInstruction through to generateContent', async () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - systemInstruction: 'be friendly' - }); + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + systemInstruction: 'be friendly' + }, + {}, + fakeChromeAdapter + ); expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); const mockResponse = getMockResponse( 'vertexAI', @@ -112,21 +146,28 @@ describe('GenerativeModel', () => { restore(); }); it('generateContent overrides model values', async () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - tools: [ - { - functionDeclarations: [ - { - name: 'myfunc', - description: 'mydesc' - } - ] - } - ], - toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, - systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } - }); + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + tools: [ + { + functionDeclarations: [ + { + name: 'myfunc', + description: 'mydesc' + } + ] + } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.NONE } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + }, + {}, + fakeChromeAdapter + ); expect(genModel.tools?.length).to.equal(1); expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( FunctionCallingMode.NONE @@ -146,7 +187,9 @@ describe('GenerativeModel', () => { functionDeclarations: [ { name: 'otherfunc', description: 'otherdesc' } ] - } + }, + { googleSearch: {} }, + { codeExecution: {} } ], toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.AUTO } }, systemInstruction: { role: 'system', parts: [{ text: 'be formal' }] } @@ -159,6 +202,8 @@ describe('GenerativeModel', () => { match((value: string) => { return ( value.includes('otherfunc') && + value.includes('googleSearch') && + value.includes('codeExecution') && value.includes(FunctionCallingMode.AUTO) && value.includes('be formal') ); @@ -168,12 +213,17 @@ describe('GenerativeModel', () => { restore(); }); it('passes base model params through to ChatSession when there are no startChatParams', async () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - generationConfig: { - topK: 1 - } - }); + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + generationConfig: { + topK: 1 + } + }, + {}, + fakeChromeAdapter + ); const chatSession = genModel.startChat(); expect(chatSession.params?.generationConfig).to.deep.equal({ topK: 1 @@ -181,12 +231,17 @@ describe('GenerativeModel', () => { restore(); }); it('overrides base model params with startChatParams', () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - generationConfig: { - topK: 1 - } - }); + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + generationConfig: { + topK: 1 + } + }, + {}, + fakeChromeAdapter + ); const chatSession = genModel.startChat({ generationConfig: { topK: 2 @@ -197,18 +252,27 @@ describe('GenerativeModel', () => { }); }); it('passes params through to chat.sendMessage', async () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - tools: [ - { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] } - ], - toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, - systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, - generationConfig: { - topK: 1 - } - }); - expect(genModel.tools?.length).to.equal(1); + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + tools: [ + { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] }, + { googleSearch: {} }, + { codeExecution: {} } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.NONE } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, + generationConfig: { + topK: 1 + } + }, + {}, + fakeChromeAdapter + ); + expect(genModel.tools?.length).to.equal(3); expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( FunctionCallingMode.NONE ); @@ -229,6 +293,8 @@ describe('GenerativeModel', () => { match((value: string) => { return ( value.includes('myfunc') && + value.includes('googleSearch') && + value.includes('codeExecution') && value.includes(FunctionCallingMode.NONE) && value.includes('be friendly') && value.includes('topK') @@ -239,10 +305,15 @@ describe('GenerativeModel', () => { restore(); }); it('passes text-only systemInstruction through to chat.sendMessage', async () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - systemInstruction: 'be friendly' - }); + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + systemInstruction: 'be friendly' + }, + {}, + fakeChromeAdapter + ); expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); const mockResponse = getMockResponse( 'vertexAI', @@ -265,17 +336,24 @@ describe('GenerativeModel', () => { restore(); }); it('startChat overrides model values', async () => { - const genModel = new GenerativeModel(fakeAI, { - model: 'my-model', - tools: [ - { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] } - ], - toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, - systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, - generationConfig: { - responseMimeType: 'image/jpeg' - } - }); + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + tools: [ + { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.NONE } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, + generationConfig: { + responseMimeType: 'image/jpeg' + } + }, + {}, + fakeChromeAdapter + ); expect(genModel.tools?.length).to.equal(1); expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( FunctionCallingMode.NONE @@ -295,7 +373,9 @@ describe('GenerativeModel', () => { functionDeclarations: [ { name: 'otherfunc', description: 'otherdesc' } ] - } + }, + { googleSearch: {} }, + { codeExecution: {} } ], toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.AUTO } @@ -314,6 +394,8 @@ describe('GenerativeModel', () => { match((value: string) => { return ( value.includes('otherfunc') && + value.includes('googleSearch') && + value.includes('codeExecution') && value.includes(FunctionCallingMode.AUTO) && value.includes('be formal') && value.includes('image/png') && @@ -325,7 +407,12 @@ describe('GenerativeModel', () => { restore(); }); it('calls countTokens', async () => { - const genModel = new GenerativeModel(fakeAI, { model: 'my-model' }); + const genModel = new GenerativeModel( + fakeAI, + { model: 'my-model' }, + {}, + fakeChromeAdapter + ); const mockResponse = getMockResponse( 'vertexAI', 'unary-success-total-tokens.json' @@ -346,3 +433,299 @@ describe('GenerativeModel', () => { restore(); }); }); + +describe('GenerativeModel dispatch logic', () => { + let makeRequestStub: SinonStub; + let mockChromeAdapter: ChromeAdapterImpl; + + function stubMakeRequest(stream?: boolean): void { + if (stream) { + makeRequestStub = stub(request, 'makeRequest').resolves( + getMockResponseStreaming( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ) as Response + ); + } else { + makeRequestStub = stub(request, 'makeRequest').resolves( + getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ) as Response + ); + } + } + + beforeEach(() => { + // @ts-ignore + mockChromeAdapter = { + isAvailable: stub(), + generateContent: stub().resolves(new Response(JSON.stringify({}))), + generateContentStream: stub().resolves( + new Response(new ReadableStream()) + ), + countTokens: stub().resolves(new Response(JSON.stringify({}))), + mode: InferenceMode.PREFER_ON_DEVICE + }; + }); + + afterEach(() => { + restore(); + }); + + describe('PREFER_ON_DEVICE', () => { + beforeEach(() => { + mockChromeAdapter.mode = InferenceMode.PREFER_ON_DEVICE; + }); + it('should use on-device for generateContent when available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(mockChromeAdapter.generateContent).to.have.been.calledOnce; + expect(makeRequestStub).to.not.have.been.called; + }); + it('should use cloud for generateContent when on-device is not available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(false); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + expect(makeRequestStub).to.have.been.calledOnce; + }); + it('should use on-device for generateContentStream when available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(mockChromeAdapter.generateContentStream).to.have.been.calledOnce; + expect(makeRequestStub).to.not.have.been.called; + }); + it('should use cloud for generateContentStream when on-device is not available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(false); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(mockChromeAdapter.generateContentStream).to.not.have.been.called; + expect(makeRequestStub).to.have.been.calledOnce; + }); + it('should use cloud for countTokens', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.countTokens('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + }); + }); + + describe('ONLY_ON_DEVICE', () => { + beforeEach(() => { + mockChromeAdapter.mode = InferenceMode.ONLY_ON_DEVICE; + }); + it('should use on-device for generateContent when available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(mockChromeAdapter.generateContent).to.have.been.calledOnce; + expect(makeRequestStub).to.not.have.been.called; + }); + it('generateContent should throw when on-device is not available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(false); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await expect(model.generateContent('hello')).to.be.rejectedWith( + /on-device model is not available/ + ); + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + expect(makeRequestStub).to.not.have.been.called; + }); + it('should use on-device for generateContentStream when available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(mockChromeAdapter.generateContentStream).to.have.been.calledOnce; + expect(makeRequestStub).to.not.have.been.called; + }); + it('generateContentStream should throw when on-device is not available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(false); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await expect(model.generateContentStream('hello')).to.be.rejectedWith( + /on-device model is not available/ + ); + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + expect(makeRequestStub).to.not.have.been.called; + }); + it('should always throw for countTokens', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await expect(model.countTokens('hello')).to.be.rejectedWith(AIError); + expect(makeRequestStub).to.not.have.been.called; + }); + }); + + describe('ONLY_IN_CLOUD', () => { + beforeEach(() => { + mockChromeAdapter.mode = InferenceMode.ONLY_IN_CLOUD; + }); + it('should use cloud for generateContent even when on-device is available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + }); + it('should use cloud for generateContentStream even when on-device is available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContentStream).to.not.have.been.called; + }); + it('should always use cloud for countTokens', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.countTokens('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + }); + }); + + describe('PREFER_IN_CLOUD', () => { + beforeEach(() => { + mockChromeAdapter.mode = InferenceMode.PREFER_IN_CLOUD; + }); + it('should use cloud for generateContent when available', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + }); + it('should fall back to on-device for generateContent if cloud fails', async () => { + makeRequestStub.rejects( + new AIError(AIErrorCode.FETCH_ERROR, 'Network error') + ); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContent).to.have.been.calledOnce; + }); + it('should use cloud for generateContentStream when available', async () => { + stubMakeRequest(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContentStream).to.not.have.been.called; + }); + it('should fall back to on-device for generateContentStream if cloud fails', async () => { + makeRequestStub.rejects( + new AIError(AIErrorCode.FETCH_ERROR, 'Network error') + ); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContentStream).to.have.been.calledOnce; + }); + it('should use cloud for countTokens', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.countTokens('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + }); + }); +}); diff --git a/packages/ai/src/models/generative-model.ts b/packages/ai/src/models/generative-model.ts index b09a9290aa4..ffce645eeb1 100644 --- a/packages/ai/src/models/generative-model.ts +++ b/packages/ai/src/models/generative-model.ts @@ -43,6 +43,7 @@ import { } from '../requests/request-helpers'; import { AI } from '../public-types'; import { AIModel } from './ai-model'; +import { ChromeAdapter } from '../types/chrome-adapter'; /** * Class for generative model APIs. @@ -59,7 +60,8 @@ export class GenerativeModel extends AIModel { constructor( ai: AI, modelParams: ModelParams, - requestOptions?: RequestOptions + requestOptions?: RequestOptions, + private chromeAdapter?: ChromeAdapter ) { super(ai, modelParams.model); this.generationConfig = modelParams.generationConfig || {}; @@ -91,6 +93,7 @@ export class GenerativeModel extends AIModel { systemInstruction: this.systemInstruction, ...formattedParams }, + this.chromeAdapter, this.requestOptions ); } @@ -116,6 +119,7 @@ export class GenerativeModel extends AIModel { systemInstruction: this.systemInstruction, ...formattedParams }, + this.chromeAdapter, this.requestOptions ); } @@ -128,6 +132,7 @@ export class GenerativeModel extends AIModel { return new ChatSession( this._apiSettings, this.model, + this.chromeAdapter, { tools: this.tools, toolConfig: this.toolConfig, @@ -152,6 +157,11 @@ export class GenerativeModel extends AIModel { request: CountTokensRequest | string | Array ): Promise { const formattedParams = formatGenerateContentInput(request); - return countTokens(this._apiSettings, this.model, formattedParams); + return countTokens( + this._apiSettings, + this.model, + formattedParams, + this.chromeAdapter + ); } } diff --git a/packages/ai/src/models/imagen-model.ts b/packages/ai/src/models/imagen-model.ts index 3c76a1c721c..a41a03f25cf 100644 --- a/packages/ai/src/models/imagen-model.ts +++ b/packages/ai/src/models/imagen-model.ts @@ -50,7 +50,7 @@ import { AIModel } from './ai-model'; * } * ``` * - * @beta + * @public */ export class ImagenModel extends AIModel { /** @@ -99,7 +99,7 @@ export class ImagenModel extends AIModel { * returned object will have a `filteredReason` property. * If all images are filtered, the `images` array will be empty. * - * @beta + * @public */ async generateImages( prompt: string diff --git a/packages/ai/src/models/index.ts b/packages/ai/src/models/index.ts index cb694a5360b..5d2492d6784 100644 --- a/packages/ai/src/models/index.ts +++ b/packages/ai/src/models/index.ts @@ -17,4 +17,5 @@ export * from './ai-model'; export * from './generative-model'; +export * from './live-generative-model'; export * from './imagen-model'; diff --git a/packages/ai/src/models/live-generative-model.test.ts b/packages/ai/src/models/live-generative-model.test.ts new file mode 100644 index 00000000000..495f340b846 --- /dev/null +++ b/packages/ai/src/models/live-generative-model.test.ts @@ -0,0 +1,171 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { use, expect } from 'chai'; +import sinon, { SinonFakeTimers, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { AI } from '../public-types'; +import { LiveSession } from '../methods/live-session'; +import { WebSocketHandler } from '../websocket'; +import { GoogleAIBackend } from '../backend'; +import { LiveGenerativeModel } from './live-generative-model'; +import { AIError } from '../errors'; + +use(sinonChai); +use(chaiAsPromised); + +// A controllable mock for the WebSocketHandler interface +class MockWebSocketHandler implements WebSocketHandler { + connect = stub().resolves(); + send = stub(); + close = stub().resolves(); + + private serverMessages: unknown[] = []; + private generatorController: { + resolve: () => void; + promise: Promise; + } | null = null; + + async *listen(): AsyncGenerator { + while (true) { + if (this.serverMessages.length > 0) { + yield this.serverMessages.shift(); + } else { + const promise = new Promise(resolve => { + this.generatorController = { resolve, promise: null! }; + }); + await promise; + } + } + } + + // Test method to simulate a message from the server + simulateServerMessage(message: object): void { + this.serverMessages.push(message); + if (this.generatorController) { + this.generatorController.resolve(); + this.generatorController = null; + } + } +} + +const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project', + appId: 'my-appid' + } + }, + backend: new GoogleAIBackend(), + location: 'us-central1' +}; + +describe('LiveGenerativeModel', () => { + let mockHandler: MockWebSocketHandler; + let clock: SinonFakeTimers; + + beforeEach(() => { + mockHandler = new MockWebSocketHandler(); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + it('connect() should call handler.connect and send setup message', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { model: 'my-model' }, + mockHandler + ); + const connectPromise = model.connect(); + + // Ensure connect was called before simulating server response + expect(mockHandler.connect).to.have.been.calledOnce; + + // Wait for the setup message to be sent + await clock.runAllAsync(); + + expect(mockHandler.send).to.have.been.calledOnce; + const setupMessage = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(setupMessage.setup.model).to.include('my-model'); + + // Simulate successful handshake and resolve the promise + mockHandler.simulateServerMessage({ setupComplete: true }); + const session = await connectPromise; + expect(session).to.be.an.instanceOf(LiveSession); + await session.close(); + }); + + it('connect() should throw if handshake fails', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { model: 'my-model' }, + mockHandler + ); + const connectPromise = model.connect(); + + // Wait for setup message + await clock.runAllAsync(); + + // Simulate a failed handshake + mockHandler.simulateServerMessage({ error: 'handshake failed' }); + await expect(connectPromise).to.be.rejectedWith( + AIError, + /Server connection handshake failed/ + ); + }); + + it('connect() should pass through connection errors', async () => { + mockHandler.connect.rejects(new Error('Connection refused')); + const model = new LiveGenerativeModel( + fakeAI, + { model: 'my-model' }, + mockHandler + ); + await expect(model.connect()).to.be.rejectedWith('Connection refused'); + }); + + it('connect() should pass through setup parameters correctly', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { + model: 'gemini-pro', + generationConfig: { temperature: 0.8 }, + systemInstruction: { role: 'system', parts: [{ text: 'Be a pirate' }] } + }, + mockHandler + ); + const connectPromise = model.connect(); + + // Wait for setup message + await clock.runAllAsync(); + + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData.setup.generationConfig).to.deep.equal({ temperature: 0.8 }); + expect(sentData.setup.systemInstruction.parts[0].text).to.equal( + 'Be a pirate' + ); + mockHandler.simulateServerMessage({ setupComplete: true }); + await connectPromise; + }); +}); diff --git a/packages/ai/src/models/live-generative-model.ts b/packages/ai/src/models/live-generative-model.ts new file mode 100644 index 00000000000..251df095202 --- /dev/null +++ b/packages/ai/src/models/live-generative-model.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIModel } from './ai-model'; +import { LiveSession } from '../methods/live-session'; +import { AIError } from '../errors'; +import { + AI, + AIErrorCode, + BackendType, + Content, + LiveGenerationConfig, + LiveModelParams, + Tool, + ToolConfig +} from '../public-types'; +import { WebSocketHandler } from '../websocket'; +import { WebSocketUrl } from '../requests/request'; +import { formatSystemInstruction } from '../requests/request-helpers'; +import { _LiveClientSetup } from '../types/live-responses'; + +/** + * Class for Live generative model APIs. The Live API enables low-latency, two-way multimodal + * interactions with Gemini. + * + * This class should only be instantiated with {@link getLiveGenerativeModel}. + * + * @beta + */ +export class LiveGenerativeModel extends AIModel { + generationConfig: LiveGenerationConfig; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: Content; + + /** + * @internal + */ + constructor( + ai: AI, + modelParams: LiveModelParams, + /** + * @internal + */ + private _webSocketHandler: WebSocketHandler + ) { + super(ai, modelParams.model); + this.generationConfig = modelParams.generationConfig || {}; + this.tools = modelParams.tools; + this.toolConfig = modelParams.toolConfig; + this.systemInstruction = formatSystemInstruction( + modelParams.systemInstruction + ); + } + + /** + * Starts a {@link LiveSession}. + * + * @returns A {@link LiveSession}. + * @throws If the connection failed to be established with the server. + * + * @beta + */ + async connect(): Promise { + const url = new WebSocketUrl(this._apiSettings); + await this._webSocketHandler.connect(url.toString()); + + let fullModelPath: string; + if (this._apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + fullModelPath = `projects/${this._apiSettings.project}/${this.model}`; + } else { + fullModelPath = `projects/${this._apiSettings.project}/locations/${this._apiSettings.location}/${this.model}`; + } + + const setupMessage: _LiveClientSetup = { + setup: { + model: fullModelPath, + generationConfig: this.generationConfig, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction + } + }; + + try { + // Begin listening for server messages, and begin the handshake by sending the 'setupMessage' + const serverMessages = this._webSocketHandler.listen(); + this._webSocketHandler.send(JSON.stringify(setupMessage)); + + // Verify we received the handshake response 'setupComplete' + const firstMessage = (await serverMessages.next()).value; + if ( + !firstMessage || + !(typeof firstMessage === 'object') || + !('setupComplete' in firstMessage) + ) { + await this._webSocketHandler.close(1011, 'Handshake failure'); + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + 'Server connection handshake failed. The server did not respond with a setupComplete message.' + ); + } + + return new LiveSession(this._webSocketHandler, serverMessages); + } catch (e) { + // Ensure connection is closed on any setup error + await this._webSocketHandler.close(); + throw e; + } + } +} diff --git a/packages/ai/src/public-types.ts b/packages/ai/src/public-types.ts index 7a5b7dc3997..fff41251a01 100644 --- a/packages/ai/src/public-types.ts +++ b/packages/ai/src/public-types.ts @@ -20,26 +20,6 @@ import { Backend } from './backend'; export * from './types'; -/** - * @deprecated Use the new {@link AI | AI} instead. The Vertex AI in Firebase SDK has been - * replaced with the Firebase AI SDK to accommodate the evolving set of supported features and - * services. For migration details, see the {@link https://firebase.google.com/docs/vertex-ai/migrate-to-latest-sdk | migration guide}. - * - * An instance of the Firebase AI SDK. - * - * @public - */ -export type VertexAI = AI; - -/** - * Options when initializing the Firebase AI SDK. - * - * @public - */ -export interface VertexAIOptions { - location?: string; -} - /** * An instance of the Firebase AI SDK. * @@ -58,6 +38,10 @@ export interface AI { * Vertex AI Gemini API (using {@link VertexAIBackend}). */ backend: Backend; + /** + * Options applied to this {@link AI} instance. + */ + options?: AIOptions; /** * @deprecated use `AI.backend.location` instead. * @@ -110,6 +94,11 @@ export type BackendType = (typeof BackendType)[keyof typeof BackendType]; export interface AIOptions { /** * The backend configuration to use for the AI service instance. + * Defaults to the Gemini Developer API backend ({@link GoogleAIBackend}). */ - backend: Backend; + backend?: Backend; + /** + * Whether to use App Check limited use tokens. Defaults to false. + */ + useLimitedUseAppCheckTokens?: boolean; } diff --git a/packages/ai/src/requests/hybrid-helpers.test.ts b/packages/ai/src/requests/hybrid-helpers.test.ts new file mode 100644 index 00000000000..a758f34ad21 --- /dev/null +++ b/packages/ai/src/requests/hybrid-helpers.test.ts @@ -0,0 +1,187 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { use, expect } from 'chai'; +import { SinonStub, SinonStubbedInstance, restore, stub } from 'sinon'; +import { callCloudOrDevice } from './hybrid-helpers'; +import { GenerateContentRequest, InferenceMode, AIErrorCode } from '../types'; +import { AIError } from '../errors'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { ChromeAdapterImpl } from '../methods/chrome-adapter'; + +use(sinonChai); +use(chaiAsPromised); + +describe('callCloudOrDevice', () => { + let chromeAdapter: SinonStubbedInstance; + let onDeviceCall: SinonStub; + let inCloudCall: SinonStub; + let request: GenerateContentRequest; + + beforeEach(() => { + // @ts-ignore + chromeAdapter = { + mode: InferenceMode.PREFER_ON_DEVICE, + isAvailable: stub(), + generateContent: stub(), + generateContentStream: stub(), + countTokens: stub() + }; + onDeviceCall = stub().resolves('on-device-response'); + inCloudCall = stub().resolves('in-cloud-response'); + request = { contents: [] }; + }); + + afterEach(() => { + restore(); + }); + + it('should call inCloudCall if chromeAdapter is undefined', async () => { + const result = await callCloudOrDevice( + request, + undefined, + onDeviceCall, + inCloudCall + ); + expect(result).to.equal('in-cloud-response'); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + + describe('PREFER_ON_DEVICE mode', () => { + beforeEach(() => { + chromeAdapter.mode = InferenceMode.PREFER_ON_DEVICE; + }); + + it('should call onDeviceCall if available', async () => { + chromeAdapter.isAvailable.resolves(true); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result).to.equal('on-device-response'); + expect(onDeviceCall).to.have.been.calledOnce; + expect(inCloudCall).to.not.have.been.called; + }); + + it('should call inCloudCall if not available', async () => { + chromeAdapter.isAvailable.resolves(false); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result).to.equal('in-cloud-response'); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + }); + + describe('ONLY_ON_DEVICE mode', () => { + beforeEach(() => { + chromeAdapter.mode = InferenceMode.ONLY_ON_DEVICE; + }); + + it('should call onDeviceCall if available', async () => { + chromeAdapter.isAvailable.resolves(true); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result).to.equal('on-device-response'); + expect(onDeviceCall).to.have.been.calledOnce; + expect(inCloudCall).to.not.have.been.called; + }); + + it('should throw if not available', async () => { + chromeAdapter.isAvailable.resolves(false); + await expect( + callCloudOrDevice(request, chromeAdapter, onDeviceCall, inCloudCall) + ).to.be.rejectedWith(/on-device model is not available/); + expect(inCloudCall).to.not.have.been.called; + expect(onDeviceCall).to.not.have.been.called; + }); + }); + + describe('ONLY_IN_CLOUD mode', () => { + beforeEach(() => { + chromeAdapter.mode = InferenceMode.ONLY_IN_CLOUD; + }); + + it('should call inCloudCall even if on-device is available', async () => { + chromeAdapter.isAvailable.resolves(true); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result).to.equal('in-cloud-response'); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + }); + + describe('PREFER_IN_CLOUD mode', () => { + beforeEach(() => { + chromeAdapter.mode = InferenceMode.PREFER_IN_CLOUD; + }); + + it('should call inCloudCall first', async () => { + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result).to.equal('in-cloud-response'); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + + it('should fall back to onDeviceCall if inCloudCall fails with AIErrorCode.FETCH_ERROR', async () => { + inCloudCall.rejects( + new AIError(AIErrorCode.FETCH_ERROR, 'Network error') + ); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result).to.equal('on-device-response'); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.have.been.calledOnce; + }); + + it('should re-throw other errors from inCloudCall', async () => { + const error = new AIError(AIErrorCode.RESPONSE_ERROR, 'safety problem'); + inCloudCall.rejects(error); + await expect( + callCloudOrDevice(request, chromeAdapter, onDeviceCall, inCloudCall) + ).to.be.rejectedWith(error); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + }); +}); diff --git a/packages/ai/src/requests/hybrid-helpers.ts b/packages/ai/src/requests/hybrid-helpers.ts new file mode 100644 index 00000000000..3140594c00e --- /dev/null +++ b/packages/ai/src/requests/hybrid-helpers.ts @@ -0,0 +1,88 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { + GenerateContentRequest, + InferenceMode, + AIErrorCode, + ChromeAdapter +} from '../types'; +import { ChromeAdapterImpl } from '../methods/chrome-adapter'; + +const errorsCausingFallback: AIErrorCode[] = [ + // most network errors + AIErrorCode.FETCH_ERROR, + // fallback code for all other errors in makeRequest + AIErrorCode.ERROR, + // error due to API not being enabled in project + AIErrorCode.API_NOT_ENABLED +]; + +/** + * Dispatches a request to the appropriate backend (on-device or in-cloud) + * based on the inference mode. + * + * @param request - The request to be sent. + * @param chromeAdapter - The on-device model adapter. + * @param onDeviceCall - The function to call for on-device inference. + * @param inCloudCall - The function to call for in-cloud inference. + * @returns The response from the backend. + */ +export async function callCloudOrDevice( + request: GenerateContentRequest, + chromeAdapter: ChromeAdapter | undefined, + onDeviceCall: () => Promise, + inCloudCall: () => Promise +): Promise { + if (!chromeAdapter) { + return inCloudCall(); + } + switch ((chromeAdapter as ChromeAdapterImpl).mode) { + case InferenceMode.ONLY_ON_DEVICE: + if (await chromeAdapter.isAvailable(request)) { + return onDeviceCall(); + } + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'Inference mode is ONLY_ON_DEVICE, but an on-device model is not available.' + ); + case InferenceMode.ONLY_IN_CLOUD: + return inCloudCall(); + case InferenceMode.PREFER_IN_CLOUD: + try { + return await inCloudCall(); + } catch (e) { + if (e instanceof AIError && errorsCausingFallback.includes(e.code)) { + return onDeviceCall(); + } + throw e; + } + case InferenceMode.PREFER_ON_DEVICE: + if (await chromeAdapter.isAvailable(request)) { + return onDeviceCall(); + } + return inCloudCall(); + default: + throw new AIError( + AIErrorCode.ERROR, + `Unexpected infererence mode: ${ + (chromeAdapter as ChromeAdapterImpl).mode + }` + ); + } +} diff --git a/packages/ai/src/requests/imagen-image-format.ts b/packages/ai/src/requests/imagen-image-format.ts index b9690a7d39b..e07d4cec818 100644 --- a/packages/ai/src/requests/imagen-image-format.ts +++ b/packages/ai/src/requests/imagen-image-format.ts @@ -32,7 +32,7 @@ import { logger } from '../logger'; * } * ``` * - * @beta + * @public */ export class ImagenImageFormat { /** @@ -54,7 +54,7 @@ export class ImagenImageFormat { * @param compressionQuality - The level of compression (a number between 0 and 100). * @returns An {@link ImagenImageFormat} object for a JPEG image. * - * @beta + * @public */ static jpeg(compressionQuality?: number): ImagenImageFormat { if ( @@ -73,7 +73,7 @@ export class ImagenImageFormat { * * @returns An {@link ImagenImageFormat} object for a PNG image. * - * @beta + * @public */ static png(): ImagenImageFormat { return { mimeType: 'image/png' }; diff --git a/packages/ai/src/requests/request-helpers.test.ts b/packages/ai/src/requests/request-helpers.test.ts index d8337850925..993b0f1d3ae 100644 --- a/packages/ai/src/requests/request-helpers.test.ts +++ b/packages/ai/src/requests/request-helpers.test.ts @@ -214,6 +214,7 @@ describe('request formatting methods', () => { expect(body.instances[0].prompt).to.equal(prompt); expect(body.parameters.sampleCount).to.equal(1); expect(body.parameters.includeRaiReason).to.be.true; + expect(body.parameters.includeSafetyAttributes).to.be.true; // Parameters without default values should be undefined expect(body.parameters.storageUri).to.be.undefined; @@ -258,6 +259,7 @@ describe('request formatting methods', () => { personGeneration: safetySettings.personFilterLevel, aspectRatio, includeRaiReason: true, + includeSafetyAttributes: true, storageUri: undefined }); }); diff --git a/packages/ai/src/requests/request-helpers.ts b/packages/ai/src/requests/request-helpers.ts index c4cc1a20acc..ee80142481b 100644 --- a/packages/ai/src/requests/request-helpers.ts +++ b/packages/ai/src/requests/request-helpers.ts @@ -156,7 +156,8 @@ export function createPredictRequestBody( addWatermark, safetyFilterLevel, personGeneration: personFilterLevel, - includeRaiReason: true + includeRaiReason: true, + includeSafetyAttributes: true } }; return body; diff --git a/packages/ai/src/requests/request.ts b/packages/ai/src/requests/request.ts index 31c5e9b8125..90195b4b788 100644 --- a/packages/ai/src/requests/request.ts +++ b/packages/ai/src/requests/request.ts @@ -20,13 +20,14 @@ import { AIError } from '../errors'; import { ApiSettings } from '../types/internal'; import { DEFAULT_API_VERSION, - DEFAULT_BASE_URL, + DEFAULT_DOMAIN, DEFAULT_FETCH_TIMEOUT_MS, LANGUAGE_TAG, PACKAGE_VERSION } from '../constants'; import { logger } from '../logger'; import { GoogleAIBackend, VertexAIBackend } from '../backend'; +import { BackendType } from '../public-types'; export enum Task { GENERATE_CONTENT = 'generateContent', @@ -51,7 +52,7 @@ export class RequestUrl { } private get baseUrl(): string { - return this.requestOptions?.baseUrl || DEFAULT_BASE_URL; + return this.requestOptions?.baseUrl || `https://${DEFAULT_DOMAIN}`; } private get apiVersion(): string { @@ -81,6 +82,28 @@ export class RequestUrl { } } +export class WebSocketUrl { + constructor(public apiSettings: ApiSettings) {} + toString(): string { + const url = new URL(`wss://${DEFAULT_DOMAIN}`); + url.pathname = this.pathname; + + const queryParams = new URLSearchParams(); + queryParams.set('key', this.apiSettings.apiKey); + url.search = queryParams.toString(); + + return url.toString(); + } + + private get pathname(): string { + if (this.apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + return 'ws/google.firebase.vertexai.v1beta.GenerativeService/BidiGenerateContent'; + } else { + return `ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/${this.apiSettings.location}`; + } + } +} + /** * Log language and "fire/version" to x-goog-api-client */ @@ -185,6 +208,7 @@ export async function makeRequest( } if ( response.status === 403 && + errorDetails && errorDetails.some( (detail: ErrorDetails) => detail.reason === 'SERVICE_DISABLED' ) && diff --git a/packages/ai/src/requests/response-helpers.test.ts b/packages/ai/src/requests/response-helpers.test.ts index 97dd2f9fe30..8583ca9a733 100644 --- a/packages/ai/src/requests/response-helpers.test.ts +++ b/packages/ai/src/requests/response-helpers.test.ts @@ -48,6 +48,21 @@ const fakeResponseText: GenerateContentResponse = { ] }; +const fakeResponseThoughts: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [ + { text: 'Some text' }, + { text: 'and some thoughts', thought: true } + ] + } + } + ] +}; + const functionCallPart1 = { functionCall: { name: 'find_theaters', @@ -188,6 +203,7 @@ describe('response-helpers methods', () => { expect(enhancedResponse.text()).to.equal('Some text and some more text'); expect(enhancedResponse.functionCalls()).to.be.undefined; expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; }); it('good response functionCall', async () => { const enhancedResponse = addHelpers(fakeResponseFunctionCall); @@ -196,6 +212,7 @@ describe('response-helpers methods', () => { functionCallPart1.functionCall ]); expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; }); it('good response functionCalls', async () => { const enhancedResponse = addHelpers(fakeResponseFunctionCalls); @@ -205,6 +222,7 @@ describe('response-helpers methods', () => { functionCallPart2.functionCall ]); expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; }); it('good response text/functionCall', async () => { const enhancedResponse = addHelpers(fakeResponseMixed1); @@ -213,6 +231,7 @@ describe('response-helpers methods', () => { ]); expect(enhancedResponse.text()).to.equal('some text'); expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; }); it('good response functionCall/text', async () => { const enhancedResponse = addHelpers(fakeResponseMixed2); @@ -221,6 +240,7 @@ describe('response-helpers methods', () => { ]); expect(enhancedResponse.text()).to.equal('some text'); expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; }); it('good response text/functionCall/text', async () => { const enhancedResponse = addHelpers(fakeResponseMixed3); @@ -228,17 +248,20 @@ describe('response-helpers methods', () => { functionCallPart1.functionCall ]); expect(enhancedResponse.text()).to.equal('some text and more text'); + expect(enhancedResponse.thoughtSummary()).to.be.undefined; expect(enhancedResponse.inlineDataParts()).to.be.undefined; }); it('bad response safety', async () => { const enhancedResponse = addHelpers(badFakeResponse); expect(enhancedResponse.text).to.throw('SAFETY'); + expect(enhancedResponse.thoughtSummary).to.throw('SAFETY'); expect(enhancedResponse.functionCalls).to.throw('SAFETY'); expect(enhancedResponse.inlineDataParts).to.throw('SAFETY'); }); it('good response inlineData', async () => { const enhancedResponse = addHelpers(fakeResponseInlineData); expect(enhancedResponse.text()).to.equal(''); + expect(enhancedResponse.thoughtSummary()).to.be.undefined; expect(enhancedResponse.functionCalls()).to.be.undefined; expect(enhancedResponse.inlineDataParts()).to.deep.equal([ inlineDataPart1, @@ -248,11 +271,19 @@ describe('response-helpers methods', () => { it('good response text/inlineData', async () => { const enhancedResponse = addHelpers(fakeResponseTextAndInlineData); expect(enhancedResponse.text()).to.equal('Describe this:'); + expect(enhancedResponse.thoughtSummary()).to.be.undefined; expect(enhancedResponse.functionCalls()).to.be.undefined; expect(enhancedResponse.inlineDataParts()).to.deep.equal([ inlineDataPart1 ]); }); + it('good response text/thought', async () => { + const enhancedResponse = addHelpers(fakeResponseThoughts); + expect(enhancedResponse.text()).to.equal('Some text'); + expect(enhancedResponse.thoughtSummary()).to.equal('and some thoughts'); + expect(enhancedResponse.functionCalls()).to.be.undefined; + expect(enhancedResponse.inlineDataParts()).to.be.undefined; + }); }); describe('getBlockString', () => { it('has no promptFeedback or bad finishReason', async () => { diff --git a/packages/ai/src/requests/response-helpers.ts b/packages/ai/src/requests/response-helpers.ts index 20678eeea68..930bfabb2ae 100644 --- a/packages/ai/src/requests/response-helpers.ts +++ b/packages/ai/src/requests/response-helpers.ts @@ -24,12 +24,43 @@ import { ImagenGCSImage, ImagenInlineImage, AIErrorCode, - InlineDataPart + InlineDataPart, + Part } from '../types'; import { AIError } from '../errors'; import { logger } from '../logger'; import { ImagenResponseInternal } from '../types/internal'; +/** + * Check that at least one candidate exists and does not have a bad + * finish reason. Warns if multiple candidates exist. + */ +function hasValidCandidates(response: GenerateContentResponse): boolean { + if (response.candidates && response.candidates.length > 0) { + if (response.candidates.length > 1) { + logger.warn( + `This response had ${response.candidates.length} ` + + `candidates. Returning text from the first candidate only. ` + + `Access response.candidates directly to use the other candidates.` + ); + } + if (hadBadFinishReason(response.candidates[0])) { + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + `Response error: ${formatBlockErrorMessage( + response + )}. Response body stored in error.response`, + { + response + } + ); + } + return true; + } else { + return false; + } +} + /** * Creates an EnhancedGenerateContentResponse object that has helper functions and * other modifications that improve usability. @@ -59,26 +90,8 @@ export function addHelpers( response: GenerateContentResponse ): EnhancedGenerateContentResponse { (response as EnhancedGenerateContentResponse).text = () => { - if (response.candidates && response.candidates.length > 0) { - if (response.candidates.length > 1) { - logger.warn( - `This response had ${response.candidates.length} ` + - `candidates. Returning text from the first candidate only. ` + - `Access response.candidates directly to use the other candidates.` - ); - } - if (hadBadFinishReason(response.candidates[0])) { - throw new AIError( - AIErrorCode.RESPONSE_ERROR, - `Response error: ${formatBlockErrorMessage( - response - )}. Response body stored in error.response`, - { - response - } - ); - } - return getText(response); + if (hasValidCandidates(response)) { + return getText(response, part => !part.thought); } else if (response.promptFeedback) { throw new AIError( AIErrorCode.RESPONSE_ERROR, @@ -90,28 +103,25 @@ export function addHelpers( } return ''; }; + (response as EnhancedGenerateContentResponse).thoughtSummary = () => { + if (hasValidCandidates(response)) { + const result = getText(response, part => !!part.thought); + return result === '' ? undefined : result; + } else if (response.promptFeedback) { + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + `Thought summary not available. ${formatBlockErrorMessage(response)}`, + { + response + } + ); + } + return undefined; + }; (response as EnhancedGenerateContentResponse).inlineDataParts = (): | InlineDataPart[] | undefined => { - if (response.candidates && response.candidates.length > 0) { - if (response.candidates.length > 1) { - logger.warn( - `This response had ${response.candidates.length} ` + - `candidates. Returning data from the first candidate only. ` + - `Access response.candidates directly to use the other candidates.` - ); - } - if (hadBadFinishReason(response.candidates[0])) { - throw new AIError( - AIErrorCode.RESPONSE_ERROR, - `Response error: ${formatBlockErrorMessage( - response - )}. Response body stored in error.response`, - { - response - } - ); - } + if (hasValidCandidates(response)) { return getInlineDataParts(response); } else if (response.promptFeedback) { throw new AIError( @@ -125,25 +135,7 @@ export function addHelpers( return undefined; }; (response as EnhancedGenerateContentResponse).functionCalls = () => { - if (response.candidates && response.candidates.length > 0) { - if (response.candidates.length > 1) { - logger.warn( - `This response had ${response.candidates.length} ` + - `candidates. Returning function calls from the first candidate only. ` + - `Access response.candidates directly to use the other candidates.` - ); - } - if (hadBadFinishReason(response.candidates[0])) { - throw new AIError( - AIErrorCode.RESPONSE_ERROR, - `Response error: ${formatBlockErrorMessage( - response - )}. Response body stored in error.response`, - { - response - } - ); - } + if (hasValidCandidates(response)) { return getFunctionCalls(response); } else if (response.promptFeedback) { throw new AIError( @@ -160,13 +152,20 @@ export function addHelpers( } /** - * Returns all text found in all parts of first candidate. + * Returns all text from the first candidate's parts, filtering by whether + * `partFilter()` returns true. + * + * @param response - The `GenerateContentResponse` from which to extract text. + * @param partFilter - Only return `Part`s for which this returns true */ -export function getText(response: GenerateContentResponse): string { +export function getText( + response: GenerateContentResponse, + partFilter: (part: Part) => boolean +): string { const textStrings = []; if (response.candidates?.[0].content?.parts) { for (const part of response.candidates?.[0].content?.parts) { - if (part.text) { + if (part.text && partFilter(part)) { textStrings.push(part.text); } } @@ -179,7 +178,7 @@ export function getText(response: GenerateContentResponse): string { } /** - * Returns {@link FunctionCall}s associated with first candidate. + * Returns every {@link FunctionCall} associated with first candidate. */ export function getFunctionCalls( response: GenerateContentResponse @@ -200,7 +199,7 @@ export function getFunctionCalls( } /** - * Returns {@link InlineDataPart}s in the first candidate if present. + * Returns every {@link InlineDataPart} in the first candidate if present. * * @internal */ @@ -229,7 +228,7 @@ const badFinishReasons = [FinishReason.RECITATION, FinishReason.SAFETY]; function hadBadFinishReason(candidate: GenerateContentCandidate): boolean { return ( !!candidate.finishReason && - badFinishReasons.includes(candidate.finishReason) + badFinishReasons.some(reason => reason === candidate.finishReason) ); } @@ -296,12 +295,14 @@ export async function handlePredictResponse< mimeType: prediction.mimeType, gcsURI: prediction.gcsUri } as T); + } else if (prediction.safetyAttributes) { + // Ignore safetyAttributes "prediction" to avoid throwing an error below. } else { throw new AIError( AIErrorCode.RESPONSE_ERROR, - `Predictions array in response has missing properties. Response: ${JSON.stringify( - responseJson - )}` + `Unexpected element in 'predictions' array in response: '${JSON.stringify( + prediction + )}'` ); } } diff --git a/packages/ai/src/requests/schema-builder.test.ts b/packages/ai/src/requests/schema-builder.test.ts index 27de1076c5f..e4d000a4c13 100644 --- a/packages/ai/src/requests/schema-builder.test.ts +++ b/packages/ai/src/requests/schema-builder.test.ts @@ -15,10 +15,16 @@ * limitations under the License. */ +import { AIError } from '../errors'; import { expect, use } from 'chai'; import sinonChai from 'sinon-chai'; -import { Schema } from './schema-builder'; -import { AIErrorCode } from '../types'; +import { + AnyOfSchema, + NumberSchema, + Schema, + StringSchema +} from './schema-builder'; +import { AIErrorCode, SchemaType } from '../types'; use(sinonChai); @@ -442,56 +448,200 @@ describe('Schema builder', () => { population: Schema.integer({ nullable: true }) }, optionalProperties: ['cat'] - }); + }) as any; // Cast to any to bypass TypedSchema check for testing purposes + expect(() => schema.toJSON()).to.throw( + AIError, + /Property "cat" specified in "optionalProperties" does not exist./ + ); + // Check the error code as well expect(() => schema.toJSON()).to.throw(AIErrorCode.INVALID_SCHEMA); }); - it('builds schema with minimum and maximum for integer', () => { - const schema = Schema.integer({ minimum: 5, maximum: 10, title: 'Rating' }); - expect(schema.toJSON()).to.eql({ - type: 'integer', - nullable: false, - minimum: 5, - maximum: 10, - title: 'Rating' + + describe('AnyOfSchema', () => { + it('builds an anyOf schema with basic types using Schema.anyOf()', () => { + const schema: AnyOfSchema = Schema.anyOf({ + anyOf: [Schema.string(), Schema.number()] + }); + + expect(schema).to.be.instanceOf(AnyOfSchema); + expect(schema.type).to.be.undefined; + expect(schema.nullable).to.be.false; // Default from SchemaParams + expect(schema.anyOf).to.be.an('array').with.lengthOf(2); + expect(schema.anyOf[0]).to.be.instanceOf(StringSchema); + expect(schema.anyOf[1]).to.be.instanceOf(NumberSchema); + + expect(schema.toJSON()).to.eql({ + type: undefined, + anyOf: [ + { type: 'string', nullable: false }, + { type: 'number', nullable: false } + ], + nullable: false + }); }); - }); - it('builds schema with minimum and maximum for number', () => { - const schema = Schema.number({ - minimum: 1.5, - maximum: 9.9, - title: 'Measurement' + it('builds an anyOf schema with complex types and options', () => { + const schema = Schema.anyOf({ + description: 'Can be a string or a detailed object', + nullable: true, + anyOf: [ + Schema.string({ description: 'A simple string' }), + Schema.object({ + properties: { + id: Schema.integer(), + name: Schema.string() + }, + description: 'A detailed object', + nullable: false // Explicitly set for the object schema itself + }) + ] + }); + + expect(schema.description).to.equal( + 'Can be a string or a detailed object' + ); + expect(schema.nullable).to.be.true; + expect(schema.anyOf).to.be.an('array').with.lengthOf(2); + + expect(schema.toJSON()).to.eql({ + type: undefined, + description: 'Can be a string or a detailed object', + nullable: true, + anyOf: [ + { type: 'string', description: 'A simple string', nullable: false }, + { + type: 'object', + description: 'A detailed object', + properties: { + id: { type: 'integer', nullable: false }, + name: { type: 'string', nullable: false } + }, + required: ['id', 'name'], + nullable: false + } + ] + }); }); - expect(schema.toJSON()).to.eql({ - type: 'number', - nullable: false, - minimum: 1.5, - maximum: 9.9, - title: 'Measurement' + + it('correctly overrides type to undefined even if type is passed in params', () => { + const schema = Schema.anyOf({ + type: SchemaType.STRING, + anyOf: [Schema.string(), Schema.number()] + }); + expect(schema.toJSON().type).to.be.undefined; + expect(schema.toJSON()).to.eql({ + type: undefined, // Explicitly undefined for anyOf + anyOf: [ + { type: 'string', nullable: false }, + { type: 'number', nullable: false } + ], + nullable: false // Default from SchemaParams + }); + }); + + it('toJSON() correctly serializes nested complex schemas within anyOf', () => { + const schema = Schema.anyOf({ + anyOf: [ + Schema.object({ + properties: { name: Schema.string() }, + optionalProperties: ['name'] + }), + Schema.array({ items: Schema.integer() }) + ] + }); + expect(schema.toJSON()).to.eql({ + type: undefined, + anyOf: [ + { + type: 'object', + properties: { name: { type: 'string', nullable: false } }, + nullable: false + }, + { + type: 'array', + items: { type: 'integer', nullable: false }, + nullable: false + } + ], + nullable: false + }); + }); + + it('throws an error if the anyOf array is empty', () => { + expect(() => Schema.anyOf({ anyOf: [] })).to.throw( + AIErrorCode.INVALID_SCHEMA + ); }); }); - it('builds object schema with propertyOrdering', () => { - const schema = Schema.object({ - title: 'User Data', - properties: { - name: Schema.string(), - age: Schema.integer(), - email: Schema.string() - }, - propertyOrdering: ['name', 'email', 'age'] + describe('ObjectSchema toJSON() optionalProperties edge cases', () => { + it('handles empty optionalProperties array (all properties required)', () => { + const schema = Schema.object({ + properties: { a: Schema.string(), b: Schema.integer() }, + optionalProperties: [] + }); + expect(schema.toJSON().required).to.deep.equal(['a', 'b']); }); - expect(schema.toJSON()).to.eql({ - type: 'object', - nullable: false, - title: 'User Data', - properties: { - name: { type: 'string', nullable: false }, - age: { type: 'integer', nullable: false }, - email: { type: 'string', nullable: false } - }, - required: ['name', 'age', 'email'], - propertyOrdering: ['name', 'email', 'age'] + + it('handles all properties being optional (empty required array)', () => { + const schema = Schema.object({ + properties: { a: Schema.string(), b: Schema.integer() }, + optionalProperties: ['a', 'b'] + }); + expect(schema.toJSON().required).to.be.undefined; // or empty array, depending on implementation + }); + it('builds schema with minimum and maximum for integer', () => { + const schema = Schema.integer({ + minimum: 5, + maximum: 10, + title: 'Rating' + }); + expect(schema.toJSON()).to.eql({ + type: 'integer', + nullable: false, + minimum: 5, + maximum: 10, + title: 'Rating' + }); + }); + + it('builds schema with minimum and maximum for number', () => { + const schema = Schema.number({ + minimum: 1.5, + maximum: 9.9, + title: 'Measurement' + }); + expect(schema.toJSON()).to.eql({ + type: 'number', + nullable: false, + minimum: 1.5, + maximum: 9.9, + title: 'Measurement' + }); + }); + + it('builds object schema with propertyOrdering', () => { + const schema = Schema.object({ + title: 'User Data', + properties: { + name: Schema.string(), + age: Schema.integer(), + email: Schema.string() + }, + propertyOrdering: ['name', 'email', 'age'] + }); + expect(schema.toJSON()).to.eql({ + type: 'object', + nullable: false, + title: 'User Data', + properties: { + name: { type: 'string', nullable: false }, + age: { type: 'integer', nullable: false }, + email: { type: 'string', nullable: false } + }, + required: ['name', 'age', 'email'], + propertyOrdering: ['name', 'email', 'age'] + }); }); }); }); diff --git a/packages/ai/src/requests/schema-builder.ts b/packages/ai/src/requests/schema-builder.ts index 7d9ece462b3..c3b7d29a820 100644 --- a/packages/ai/src/requests/schema-builder.ts +++ b/packages/ai/src/requests/schema-builder.ts @@ -21,8 +21,7 @@ import { SchemaInterface, SchemaType, SchemaParams, - SchemaRequest, - ObjectSchemaInterface + SchemaRequest } from '../types/schema'; /** @@ -34,10 +33,11 @@ import { */ export abstract class Schema implements SchemaInterface { /** - * Optional. The type of the property. {@link - * SchemaType}. + * Optional. The type of the property. + * This can only be undefined when using `anyOf` schemas, which do not have an + * explicit type in the {@link https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type | OpenAPI specification}. */ - type: SchemaType; + type?: SchemaType; /** Optional. The format of the property. * Supported formats:
*
    @@ -51,9 +51,9 @@ export abstract class Schema implements SchemaInterface { description?: string; /** Optional. The items of the property. */ items?: SchemaInterface; - /** The minimum number of items (elements) in a schema of type {@link SchemaType.ARRAY}. */ + /** The minimum number of items (elements) in a schema of {@link (SchemaType:type)} `array`. */ minItems?: number; - /** The maximum number of items (elements) in a schema of type {@link SchemaType.ARRAY}. */ + /** The maximum number of items (elements) in a schema of {@link (SchemaType:type)} `array`. */ maxItems?: number; /** Optional. Whether the property is nullable. Defaults to false. */ nullable: boolean; @@ -66,12 +66,22 @@ export abstract class Schema implements SchemaInterface { [key: string]: unknown; constructor(schemaParams: SchemaInterface) { + // TODO(dlarocque): Enforce this with union types + if (!schemaParams.type && !schemaParams.anyOf) { + throw new AIError( + AIErrorCode.INVALID_SCHEMA, + "A schema must have either a 'type' or an 'anyOf' array of sub-schemas." + ); + } // eslint-disable-next-line guard-for-in for (const paramKey in schemaParams) { this[paramKey] = schemaParams[paramKey]; } // Ensure these are explicitly set to avoid TS errors. this.type = schemaParams.type; + this.format = schemaParams.hasOwnProperty('format') + ? schemaParams.format + : undefined; this.nullable = schemaParams.hasOwnProperty('nullable') ? !!schemaParams.nullable : false; @@ -83,7 +93,7 @@ export abstract class Schema implements SchemaInterface { * @internal */ toJSON(): SchemaRequest { - const obj: { type: SchemaType; [key: string]: unknown } = { + const obj: { type?: SchemaType; [key: string]: unknown } = { type: this.type }; for (const prop in this) { @@ -139,6 +149,12 @@ export abstract class Schema implements SchemaInterface { static boolean(booleanParams?: SchemaParams): BooleanSchema { return new BooleanSchema(booleanParams); } + + static anyOf( + anyOfParams: SchemaParams & { anyOf: TypedSchema[] } + ): AnyOfSchema { + return new AnyOfSchema(anyOfParams); + } } /** @@ -151,7 +167,8 @@ export type TypedSchema = | StringSchema | BooleanSchema | ObjectSchema - | ArraySchema; + | ArraySchema + | AnyOfSchema; /** * Schema class for "integer" types. @@ -292,7 +309,41 @@ export class ObjectSchema extends Schema { if (required.length > 0) { obj.required = required; } - delete (obj as ObjectSchemaInterface).optionalProperties; + delete obj.optionalProperties; return obj as SchemaRequest; } } + +/** + * Schema class representing a value that can conform to any of the provided sub-schemas. This is + * useful when a field can accept multiple distinct types or structures. + * @public + */ +export class AnyOfSchema extends Schema { + anyOf: TypedSchema[]; // Re-define field to narrow to required type + constructor(schemaParams: SchemaParams & { anyOf: TypedSchema[] }) { + if (schemaParams.anyOf.length === 0) { + throw new AIError( + AIErrorCode.INVALID_SCHEMA, + "The 'anyOf' array must not be empty." + ); + } + super({ + ...schemaParams, + type: undefined // anyOf schemas do not have an explicit type + }); + this.anyOf = schemaParams.anyOf; + } + + /** + * @internal + */ + toJSON(): SchemaRequest { + const obj = super.toJSON(); + // Ensure the 'anyOf' property contains serialized SchemaRequest objects. + if (this.anyOf && Array.isArray(this.anyOf)) { + obj.anyOf = (this.anyOf as TypedSchema[]).map(s => s.toJSON()); + } + return obj; + } +} diff --git a/packages/ai/src/requests/stream-reader.test.ts b/packages/ai/src/requests/stream-reader.test.ts index f0298082f68..2e50bbb3d3e 100644 --- a/packages/ai/src/requests/stream-reader.test.ts +++ b/packages/ai/src/requests/stream-reader.test.ts @@ -194,6 +194,20 @@ describe('processStream', () => { expect(response.text()).to.equal(''); } }); + it('handles empty parts', async () => { + const fakeResponse = getMockResponseStreaming( + 'googleAI', + 'streaming-success-empty-parts.txt' + ); + + const result = processStream(fakeResponse as Response, fakeApiSettings); + for await (const response of result.stream) { + expect(response.candidates?.[0].content.parts.length).to.be.at.least(1); + } + + const aggregatedResponse = await result.response; + expect(aggregatedResponse.candidates?.[0].content.parts.length).to.equal(6); + }); it('unknown enum - should ignore', async () => { const fakeResponse = getMockResponseStreaming( 'vertexAI', diff --git a/packages/ai/src/requests/stream-reader.ts b/packages/ai/src/requests/stream-reader.ts index 543d1d02266..042c052fa82 100644 --- a/packages/ai/src/requests/stream-reader.ts +++ b/packages/ai/src/requests/stream-reader.ts @@ -28,7 +28,7 @@ import { createEnhancedContentResponse } from './response-helpers'; import * as GoogleAIMapper from '../googleai-mappers'; import { GoogleAIGenerateContentResponse } from '../types/googleai'; import { ApiSettings } from '../types/internal'; -import { BackendType } from '../public-types'; +import { BackendType, URLContextMetadata } from '../public-types'; const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/; @@ -100,6 +100,17 @@ async function* generateResponseSequence( enhancedResponse = createEnhancedContentResponse(value); } + const firstCandidate = enhancedResponse.candidates?.[0]; + // Don't yield a response with no useful data for the developer. + if ( + !firstCandidate?.content?.parts && + !firstCandidate?.finishReason && + !firstCandidate?.citationMetadata && + !firstCandidate?.urlContextMetadata + ) { + continue; + } + yield enhancedResponse; } } @@ -190,42 +201,51 @@ export function aggregateResponses( candidate.finishMessage; aggregatedResponse.candidates[i].safetyRatings = candidate.safetyRatings; + aggregatedResponse.candidates[i].groundingMetadata = + candidate.groundingMetadata; + + // The urlContextMetadata object is defined in the first chunk of the response stream. + // In all subsequent chunks, the urlContextMetadata object will be undefined. We need to + // make sure that we don't overwrite the first value urlContextMetadata object with undefined. + // FIXME: What happens if we receive a second, valid urlContextMetadata object? + const urlContextMetadata = candidate.urlContextMetadata as unknown; + if ( + typeof urlContextMetadata === 'object' && + urlContextMetadata !== null && + Object.keys(urlContextMetadata).length > 0 + ) { + aggregatedResponse.candidates[i].urlContextMetadata = + urlContextMetadata as URLContextMetadata; + } /** * Candidates should always have content and parts, but this handles * possible malformed responses. */ - if (candidate.content && candidate.content.parts) { + if (candidate.content) { + // Skip a candidate without parts. + if (!candidate.content.parts) { + continue; + } if (!aggregatedResponse.candidates[i].content) { aggregatedResponse.candidates[i].content = { role: candidate.content.role || 'user', parts: [] }; } - const newPart: Partial = {}; for (const part of candidate.content.parts) { - if (part.text !== undefined) { - // The backend can send empty text parts. If these are sent back - // (e.g. in chat history), the backend will respond with an error. - // To prevent this, ignore empty text parts. - if (part.text === '') { - continue; - } - newPart.text = part.text; - } - if (part.functionCall) { - newPart.functionCall = part.functionCall; + const newPart: Part = { ...part }; + // The backend can send empty text parts. If these are sent back + // (e.g. in chat history), the backend will respond with an error. + // To prevent this, ignore empty text parts. + if (part.text === '') { + continue; } - if (Object.keys(newPart).length === 0) { - throw new AIError( - AIErrorCode.INVALID_CONTENT, - 'Part should have at least one property, but there are none. This is likely caused ' + - 'by a malformed response from the backend.' + if (Object.keys(newPart).length > 0) { + aggregatedResponse.candidates[i].content.parts.push( + newPart as Part ); } - aggregatedResponse.candidates[i].content.parts.push( - newPart as Part - ); } } } diff --git a/packages/ai/src/service.ts b/packages/ai/src/service.ts index 006cc45a94e..0beb8dda1c3 100644 --- a/packages/ai/src/service.ts +++ b/packages/ai/src/service.ts @@ -16,7 +16,7 @@ */ import { FirebaseApp, _FirebaseService } from '@firebase/app'; -import { AI } from './public-types'; +import { AI, AIOptions, InferenceMode, OnDeviceParams } from './public-types'; import { AppCheckInternalComponentName, FirebaseAppCheckInternal @@ -27,17 +27,24 @@ import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { Backend, VertexAIBackend } from './backend'; +import { ChromeAdapterImpl } from './methods/chrome-adapter'; export class AIService implements AI, _FirebaseService { auth: FirebaseAuthInternal | null; appCheck: FirebaseAppCheckInternal | null; + _options?: Omit; location: string; // This is here for backwards-compatibility constructor( public app: FirebaseApp, public backend: Backend, authProvider?: Provider, - appCheckProvider?: Provider + appCheckProvider?: Provider, + public chromeAdapterFactory?: ( + mode: InferenceMode, + window?: Window, + params?: OnDeviceParams + ) => ChromeAdapterImpl | undefined ) { const appCheck = appCheckProvider?.getImmediate({ optional: true }); const auth = authProvider?.getImmediate({ optional: true }); @@ -54,4 +61,12 @@ export class AIService implements AI, _FirebaseService { _delete(): Promise { return Promise.resolve(); } + + set options(optionsToSet: AIOptions) { + this._options = optionsToSet; + } + + get options(): AIOptions | undefined { + return this._options; + } } diff --git a/packages/ai/src/types/chrome-adapter.ts b/packages/ai/src/types/chrome-adapter.ts new file mode 100644 index 00000000000..fc33325217f --- /dev/null +++ b/packages/ai/src/types/chrome-adapter.ts @@ -0,0 +1,61 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CountTokensRequest, GenerateContentRequest } from './requests'; + +/** + * Defines an inference "backend" that uses Chrome's on-device model, + * and encapsulates logic for detecting when on-device inference is + * possible. + * + * These methods should not be called directly by the user. + * + * @beta + */ +export interface ChromeAdapter { + /** + * Checks if the on-device model is capable of handling a given + * request. + * @param request - A potential request to be passed to the model. + */ + isAvailable(request: GenerateContentRequest): Promise; + + /** + * Generates content using on-device inference. + * + * @remarks + * This is comparable to {@link GenerativeModel.generateContent} for generating + * content using in-cloud inference. + * @param request - a standard Firebase AI {@link GenerateContentRequest} + */ + generateContent(request: GenerateContentRequest): Promise; + + /** + * Generates a content stream using on-device inference. + * + * @remarks + * This is comparable to {@link GenerativeModel.generateContentStream} for generating + * a content stream using in-cloud inference. + * @param request - a standard Firebase AI {@link GenerateContentRequest} + */ + generateContentStream(request: GenerateContentRequest): Promise; + + /** + * @internal + */ + countTokens(request: CountTokensRequest): Promise; +} diff --git a/packages/ai/src/types/content.ts b/packages/ai/src/types/content.ts index ad2906671e4..401a8cfb1a8 100644 --- a/packages/ai/src/types/content.ts +++ b/packages/ai/src/types/content.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Role } from './enums'; +import { Language, Outcome, Role } from './enums'; /** * Content type for both prompts and response candidates. @@ -36,7 +36,9 @@ export type Part = | InlineDataPart | FunctionCallPart | FunctionResponsePart - | FileDataPart; + | FileDataPart + | ExecutableCodePart + | CodeExecutionResultPart; /** * Content part interface if the part represents a text string. @@ -47,6 +49,13 @@ export interface TextPart { inlineData?: never; functionCall?: never; functionResponse?: never; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: string; + executableCode?: never; + codeExecutionResult?: never; } /** @@ -62,6 +71,13 @@ export interface InlineDataPart { * Applicable if `inlineData` is a video. */ videoMetadata?: VideoMetadata; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: never; } /** @@ -90,6 +106,13 @@ export interface FunctionCallPart { inlineData?: never; functionCall: FunctionCall; functionResponse?: never; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: never; } /** @@ -101,6 +124,13 @@ export interface FunctionResponsePart { inlineData?: never; functionCall?: never; functionResponse: FunctionResponse; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: never; } /** @@ -113,6 +143,86 @@ export interface FileDataPart { functionCall?: never; functionResponse?: never; fileData: FileData; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: never; +} + +/** + * Represents the code that is executed by the model. + * + * @beta + */ +export interface ExecutableCodePart { + text?: never; + inlineData?: never; + functionCall?: never; + functionResponse?: never; + fileData: never; + thought?: never; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: ExecutableCode; + codeExecutionResult?: never; +} + +/** + * Represents the code execution result from the model. + * + * @beta + */ +export interface CodeExecutionResultPart { + text?: never; + inlineData?: never; + functionCall?: never; + functionResponse?: never; + fileData: never; + thought?: never; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: CodeExecutionResult; +} + +/** + * An interface for executable code returned by the model. + * + * @beta + */ +export interface ExecutableCode { + /** + * The programming language of the code. + */ + language?: Language; + /** + * The source code to be executed. + */ + code?: string; +} + +/** + * The results of code execution run by the model. + * + * @beta + */ +export interface CodeExecutionResult { + /** + * The result of the code execution. + */ + outcome?: Outcome; + /** + * The output from the code execution, or an error message + * if it failed. + */ + output?: string; } /** @@ -122,6 +232,15 @@ export interface FileDataPart { * @public */ export interface FunctionCall { + /** + * The id of the function call. This must be sent back in the associated {@link FunctionResponse}. + * + * + * @remarks This property is only supported in the Gemini Developer API ({@link GoogleAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property will be + * `undefined`. + */ + id?: string; name: string; args: object; } @@ -136,6 +255,14 @@ export interface FunctionCall { * @public */ export interface FunctionResponse { + /** + * The id of the {@link FunctionCall}. + * + * @remarks This property is only supported in the Gemini Developer API ({@link GoogleAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property will be + * `undefined`. + */ + id?: string; name: string; response: object; } diff --git a/packages/ai/src/types/enums.ts b/packages/ai/src/types/enums.ts index 47d654bbcd1..cd7029df3b0 100644 --- a/packages/ai/src/types/enums.ts +++ b/packages/ai/src/types/enums.ts @@ -31,229 +31,287 @@ export const POSSIBLE_ROLES = ['user', 'model', 'function', 'system'] as const; * Harm categories that would cause prompts or candidates to be blocked. * @public */ -export enum HarmCategory { - HARM_CATEGORY_HATE_SPEECH = 'HARM_CATEGORY_HATE_SPEECH', - HARM_CATEGORY_SEXUALLY_EXPLICIT = 'HARM_CATEGORY_SEXUALLY_EXPLICIT', - HARM_CATEGORY_HARASSMENT = 'HARM_CATEGORY_HARASSMENT', - HARM_CATEGORY_DANGEROUS_CONTENT = 'HARM_CATEGORY_DANGEROUS_CONTENT' -} +export const HarmCategory = { + HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH', + HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', + HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT', + HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT' +} as const; + +/** + * Harm categories that would cause prompts or candidates to be blocked. + * @public + */ +export type HarmCategory = (typeof HarmCategory)[keyof typeof HarmCategory]; /** * Threshold above which a prompt or candidate will be blocked. * @public */ -export enum HarmBlockThreshold { +export const HarmBlockThreshold = { /** * Content with `NEGLIGIBLE` will be allowed. */ - BLOCK_LOW_AND_ABOVE = 'BLOCK_LOW_AND_ABOVE', + BLOCK_LOW_AND_ABOVE: 'BLOCK_LOW_AND_ABOVE', /** * Content with `NEGLIGIBLE` and `LOW` will be allowed. */ - BLOCK_MEDIUM_AND_ABOVE = 'BLOCK_MEDIUM_AND_ABOVE', + BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE', /** * Content with `NEGLIGIBLE`, `LOW`, and `MEDIUM` will be allowed. */ - BLOCK_ONLY_HIGH = 'BLOCK_ONLY_HIGH', + BLOCK_ONLY_HIGH: 'BLOCK_ONLY_HIGH', /** * All content will be allowed. */ - BLOCK_NONE = 'BLOCK_NONE', + BLOCK_NONE: 'BLOCK_NONE', /** * All content will be allowed. This is the same as `BLOCK_NONE`, but the metadata corresponding - * to the {@link HarmCategory} will not be present in the response. + * to the {@link (HarmCategory:type)} will not be present in the response. */ - OFF = 'OFF' -} + OFF: 'OFF' +} as const; + +/** + * Threshold above which a prompt or candidate will be blocked. + * @public + */ +export type HarmBlockThreshold = + (typeof HarmBlockThreshold)[keyof typeof HarmBlockThreshold]; /** * This property is not supported in the Gemini Developer API ({@link GoogleAIBackend}). * * @public */ -export enum HarmBlockMethod { +export const HarmBlockMethod = { /** * The harm block method uses both probability and severity scores. */ - SEVERITY = 'SEVERITY', + SEVERITY: 'SEVERITY', /** * The harm block method uses the probability score. */ - PROBABILITY = 'PROBABILITY' -} + PROBABILITY: 'PROBABILITY' +} as const; + +/** + * This property is not supported in the Gemini Developer API ({@link GoogleAIBackend}). + * + * @public + */ +export type HarmBlockMethod = + (typeof HarmBlockMethod)[keyof typeof HarmBlockMethod]; /** * Probability that a prompt or candidate matches a harm category. * @public */ -export enum HarmProbability { +export const HarmProbability = { /** * Content has a negligible chance of being unsafe. */ - NEGLIGIBLE = 'NEGLIGIBLE', + NEGLIGIBLE: 'NEGLIGIBLE', /** * Content has a low chance of being unsafe. */ - LOW = 'LOW', + LOW: 'LOW', /** * Content has a medium chance of being unsafe. */ - MEDIUM = 'MEDIUM', + MEDIUM: 'MEDIUM', /** * Content has a high chance of being unsafe. */ - HIGH = 'HIGH' -} + HIGH: 'HIGH' +} as const; + +/** + * Probability that a prompt or candidate matches a harm category. + * @public + */ +export type HarmProbability = + (typeof HarmProbability)[keyof typeof HarmProbability]; /** * Harm severity levels. * @public */ -export enum HarmSeverity { +export const HarmSeverity = { /** * Negligible level of harm severity. */ - HARM_SEVERITY_NEGLIGIBLE = 'HARM_SEVERITY_NEGLIGIBLE', + HARM_SEVERITY_NEGLIGIBLE: 'HARM_SEVERITY_NEGLIGIBLE', /** * Low level of harm severity. */ - HARM_SEVERITY_LOW = 'HARM_SEVERITY_LOW', + HARM_SEVERITY_LOW: 'HARM_SEVERITY_LOW', /** * Medium level of harm severity. */ - HARM_SEVERITY_MEDIUM = 'HARM_SEVERITY_MEDIUM', + HARM_SEVERITY_MEDIUM: 'HARM_SEVERITY_MEDIUM', /** * High level of harm severity. */ - HARM_SEVERITY_HIGH = 'HARM_SEVERITY_HIGH', + HARM_SEVERITY_HIGH: 'HARM_SEVERITY_HIGH', /** * Harm severity is not supported. * * @remarks * The GoogleAI backend does not support `HarmSeverity`, so this value is used as a fallback. */ - HARM_SEVERITY_UNSUPPORTED = 'HARM_SEVERITY_UNSUPPORTED' -} + HARM_SEVERITY_UNSUPPORTED: 'HARM_SEVERITY_UNSUPPORTED' +} as const; + +/** + * Harm severity levels. + * @public + */ +export type HarmSeverity = (typeof HarmSeverity)[keyof typeof HarmSeverity]; /** * Reason that a prompt was blocked. * @public */ -export enum BlockReason { +export const BlockReason = { /** * Content was blocked by safety settings. */ - SAFETY = 'SAFETY', + SAFETY: 'SAFETY', /** * Content was blocked, but the reason is uncategorized. */ - OTHER = 'OTHER', + OTHER: 'OTHER', /** * Content was blocked because it contained terms from the terminology blocklist. */ - BLOCKLIST = 'BLOCKLIST', + BLOCKLIST: 'BLOCKLIST', /** * Content was blocked due to prohibited content. */ - PROHIBITED_CONTENT = 'PROHIBITED_CONTENT' -} + PROHIBITED_CONTENT: 'PROHIBITED_CONTENT' +} as const; + +/** + * Reason that a prompt was blocked. + * @public + */ +export type BlockReason = (typeof BlockReason)[keyof typeof BlockReason]; /** * Reason that a candidate finished. * @public */ -export enum FinishReason { +export const FinishReason = { /** * Natural stop point of the model or provided stop sequence. */ - STOP = 'STOP', + STOP: 'STOP', /** * The maximum number of tokens as specified in the request was reached. */ - MAX_TOKENS = 'MAX_TOKENS', + MAX_TOKENS: 'MAX_TOKENS', /** * The candidate content was flagged for safety reasons. */ - SAFETY = 'SAFETY', + SAFETY: 'SAFETY', /** * The candidate content was flagged for recitation reasons. */ - RECITATION = 'RECITATION', + RECITATION: 'RECITATION', /** * Unknown reason. */ - OTHER = 'OTHER', + OTHER: 'OTHER', /** * The candidate content contained forbidden terms. */ - BLOCKLIST = 'BLOCKLIST', + BLOCKLIST: 'BLOCKLIST', /** * The candidate content potentially contained prohibited content. */ - PROHIBITED_CONTENT = 'PROHIBITED_CONTENT', + PROHIBITED_CONTENT: 'PROHIBITED_CONTENT', /** * The candidate content potentially contained Sensitive Personally Identifiable Information (SPII). */ - SPII = 'SPII', + SPII: 'SPII', /** * The function call generated by the model was invalid. */ - MALFORMED_FUNCTION_CALL = 'MALFORMED_FUNCTION_CALL' -} + MALFORMED_FUNCTION_CALL: 'MALFORMED_FUNCTION_CALL' +} as const; /** + * Reason that a candidate finished. * @public */ -export enum FunctionCallingMode { +export type FinishReason = (typeof FinishReason)[keyof typeof FinishReason]; + +/** + * @public + */ +export const FunctionCallingMode = { /** * Default model behavior; model decides to predict either a function call * or a natural language response. */ - AUTO = 'AUTO', + AUTO: 'AUTO', /** * Model is constrained to always predicting a function call only. * If `allowed_function_names` is set, the predicted function call will be * limited to any one of `allowed_function_names`, else the predicted * function call will be any one of the provided `function_declarations`. */ - ANY = 'ANY', + ANY: 'ANY', /** * Model will not predict any function call. Model behavior is same as when * not passing any function declarations. */ - NONE = 'NONE' -} + NONE: 'NONE' +} as const; + +/** + * @public + */ +export type FunctionCallingMode = + (typeof FunctionCallingMode)[keyof typeof FunctionCallingMode]; /** * Content part modality. * @public */ -export enum Modality { +export const Modality = { /** * Unspecified modality. */ - MODALITY_UNSPECIFIED = 'MODALITY_UNSPECIFIED', + MODALITY_UNSPECIFIED: 'MODALITY_UNSPECIFIED', /** * Plain text. */ - TEXT = 'TEXT', + TEXT: 'TEXT', /** * Image. */ - IMAGE = 'IMAGE', + IMAGE: 'IMAGE', /** * Video. */ - VIDEO = 'VIDEO', + VIDEO: 'VIDEO', /** * Audio. */ - AUDIO = 'AUDIO', + AUDIO: 'AUDIO', /** * Document (for example, PDF). */ - DOCUMENT = 'DOCUMENT' -} + DOCUMENT: 'DOCUMENT' +} as const; + +/** + * Content part modality. + * @public + */ +export type Modality = (typeof Modality)[keyof typeof Modality]; /** * Generation modalities to be returned in generation responses. @@ -270,7 +328,12 @@ export const ResponseModality = { * Image. * @beta */ - IMAGE: 'IMAGE' + IMAGE: 'IMAGE', + /** + * Audio. + * @beta + */ + AUDIO: 'AUDIO' } as const; /** @@ -280,3 +343,74 @@ export const ResponseModality = { */ export type ResponseModality = (typeof ResponseModality)[keyof typeof ResponseModality]; + +/** + * Determines whether inference happens on-device or in-cloud. + * + * @remarks + * PREFER_ON_DEVICE: Attempt to make inference calls using an + * on-device model. If on-device inference is not available, the SDK + * will fall back to using a cloud-hosted model. + *
    + * ONLY_ON_DEVICE: Only attempt to make inference calls using an + * on-device model. The SDK will not fall back to a cloud-hosted model. + * If on-device inference is not available, inference methods will throw. + *
    + * ONLY_IN_CLOUD: Only attempt to make inference calls using a + * cloud-hosted model. The SDK will not fall back to an on-device model. + *
    + * PREFER_IN_CLOUD: Attempt to make inference calls to a + * cloud-hosted model. If not available, the SDK will fall back to an + * on-device model. + * + * @beta + */ +export const InferenceMode = { + 'PREFER_ON_DEVICE': 'prefer_on_device', + 'ONLY_ON_DEVICE': 'only_on_device', + 'ONLY_IN_CLOUD': 'only_in_cloud', + 'PREFER_IN_CLOUD': 'prefer_in_cloud' +} as const; + +/** + * Determines whether inference happens on-device or in-cloud. + * + * @beta + */ +export type InferenceMode = (typeof InferenceMode)[keyof typeof InferenceMode]; + +/** + * Represents the result of the code execution. + * + * @beta + */ +export const Outcome = { + UNSPECIFIED: 'OUTCOME_UNSPECIFIED', + OK: 'OUTCOME_OK', + FAILED: 'OUTCOME_FAILED', + DEADLINE_EXCEEDED: 'OUTCOME_DEADLINE_EXCEEDED' +}; + +/** + * Represents the result of the code execution. + * + * @beta + */ +export type Outcome = (typeof Outcome)[keyof typeof Outcome]; + +/** + * The programming language of the code. + * + * @beta + */ +export const Language = { + UNSPECIFIED: 'LANGUAGE_UNSPECIFIED', + PYTHON: 'PYTHON' +}; + +/** + * The programming language of the code. + * + * @beta + */ +export type Language = (typeof Language)[keyof typeof Language]; diff --git a/packages/ai/src/types/error.ts b/packages/ai/src/types/error.ts index 84a30f4e872..a230f683f37 100644 --- a/packages/ai/src/types/error.ts +++ b/packages/ai/src/types/error.ts @@ -62,43 +62,53 @@ export interface CustomErrorData { * * @public */ -export const enum AIErrorCode { +export const AIErrorCode = { /** A generic error occurred. */ - ERROR = 'error', + ERROR: 'error', /** An error occurred in a request. */ - REQUEST_ERROR = 'request-error', + REQUEST_ERROR: 'request-error', /** An error occurred in a response. */ - RESPONSE_ERROR = 'response-error', + RESPONSE_ERROR: 'response-error', /** An error occurred while performing a fetch. */ - FETCH_ERROR = 'fetch-error', + FETCH_ERROR: 'fetch-error', + + /** An error occurred because an operation was attempted on a closed session. */ + SESSION_CLOSED: 'session-closed', /** An error associated with a Content object. */ - INVALID_CONTENT = 'invalid-content', + INVALID_CONTENT: 'invalid-content', /** An error due to the Firebase API not being enabled in the Console. */ - API_NOT_ENABLED = 'api-not-enabled', + API_NOT_ENABLED: 'api-not-enabled', /** An error due to invalid Schema input. */ - INVALID_SCHEMA = 'invalid-schema', + INVALID_SCHEMA: 'invalid-schema', /** An error occurred due to a missing Firebase API key. */ - NO_API_KEY = 'no-api-key', + NO_API_KEY: 'no-api-key', /** An error occurred due to a missing Firebase app ID. */ - NO_APP_ID = 'no-app-id', + NO_APP_ID: 'no-app-id', /** An error occurred due to a model name not being specified during initialization. */ - NO_MODEL = 'no-model', + NO_MODEL: 'no-model', /** An error occurred due to a missing project ID. */ - NO_PROJECT_ID = 'no-project-id', + NO_PROJECT_ID: 'no-project-id', /** An error occurred while parsing. */ - PARSE_FAILED = 'parse-failed', + PARSE_FAILED: 'parse-failed', /** An error occurred due an attempt to use an unsupported feature. */ - UNSUPPORTED = 'unsupported' -} + UNSUPPORTED: 'unsupported' +} as const; + +/** + * Standardized error codes that {@link AIError} can have. + * + * @public + */ +export type AIErrorCode = (typeof AIErrorCode)[keyof typeof AIErrorCode]; diff --git a/packages/ai/src/types/googleai.ts b/packages/ai/src/types/googleai.ts index 38c27b3fe8b..eb282b094fc 100644 --- a/packages/ai/src/types/googleai.ts +++ b/packages/ai/src/types/googleai.ts @@ -23,7 +23,8 @@ import { GroundingMetadata, PromptFeedback, SafetyRating, - UsageMetadata + UsageMetadata, + URLContextMetadata } from '../public-types'; import { Content, Part } from './content'; @@ -60,6 +61,7 @@ export interface GoogleAIGenerateContentCandidate { safetyRatings?: SafetyRating[]; citationMetadata?: GoogleAICitationMetadata; groundingMetadata?: GroundingMetadata; + urlContextMetadata?: URLContextMetadata; } /** diff --git a/packages/ai/src/types/imagen/internal.ts b/packages/ai/src/types/imagen/internal.ts index 02a8a55e01c..1a34eb18f56 100644 --- a/packages/ai/src/types/imagen/internal.ts +++ b/packages/ai/src/types/imagen/internal.ts @@ -61,6 +61,14 @@ export interface ImagenResponseInternal { * The reason why the image was filtered. */ raiFilteredReason?: string; + /** + * The safety attributes. + * + * This type is currently unused in the SDK. It is sent back because our requests set + * `includeSafetyAttributes`. This property is currently only used to avoid throwing an error + * when encountering this unsupported prediction type. + */ + safetyAttributes?: unknown; }>; } @@ -84,6 +92,7 @@ export interface ImagenResponseInternal { * "personGeneration": "allow_all", * "sampleCount": 2, * "includeRaiReason": true, + * "includeSafetyAttributes": true, * "aspectRatio": "9:16" * } * } @@ -111,6 +120,7 @@ export interface PredictRequestBody { safetyFilterLevel?: string; personGeneration?: string; // Maps to personFilterLevel includeRaiReason: boolean; + includeSafetyAttributes: boolean; }; } diff --git a/packages/ai/src/types/imagen/requests.ts b/packages/ai/src/types/imagen/requests.ts index 09bd3dedc9b..4cd59342948 100644 --- a/packages/ai/src/types/imagen/requests.ts +++ b/packages/ai/src/types/imagen/requests.ts @@ -20,7 +20,7 @@ import { ImagenImageFormat } from '../../requests/imagen-image-format'; /** * Parameters for configuring an {@link ImagenModel}. * - * @beta + * @public */ export interface ImagenModelParams { /** @@ -49,7 +49,7 @@ export interface ImagenModelParams { * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images-imagen | documentation} for * more details. * - * @beta + * @public */ export interface ImagenGenerationConfig { /** @@ -73,7 +73,7 @@ export interface ImagenGenerationConfig { numberOfImages?: number; /** * The aspect ratio of the generated images. The default value is square 1:1. - * Supported aspect ratios depend on the Imagen model, see {@link ImagenAspectRatio} + * Supported aspect ratios depend on the Imagen model, see {@link (ImagenAspectRatio:type)} * for more details. */ aspectRatio?: ImagenAspectRatio; @@ -108,29 +108,44 @@ export interface ImagenGenerationConfig { * and the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters | Responsible AI and usage guidelines} * for more details. * - * @beta + * @public */ -export enum ImagenSafetyFilterLevel { +export const ImagenSafetyFilterLevel = { /** * The most aggressive filtering level; most strict blocking. */ - BLOCK_LOW_AND_ABOVE = 'block_low_and_above', + BLOCK_LOW_AND_ABOVE: 'block_low_and_above', /** * Blocks some sensitive prompts and responses. */ - BLOCK_MEDIUM_AND_ABOVE = 'block_medium_and_above', + BLOCK_MEDIUM_AND_ABOVE: 'block_medium_and_above', /** * Blocks few sensitive prompts and responses. */ - BLOCK_ONLY_HIGH = 'block_only_high', + BLOCK_ONLY_HIGH: 'block_only_high', /** * The least aggressive filtering level; blocks very few sensitive prompts and responses. * * Access to this feature is restricted and may require your case to be reviewed and approved by * Cloud support. */ - BLOCK_NONE = 'block_none' -} + BLOCK_NONE: 'block_none' +} as const; + +/** + * A filter level controlling how aggressively to filter sensitive content. + * + * Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI + * are assessed against a list of safety filters, which include 'harmful categories' (for example, + * `violence`, `sexual`, `derogatory`, and `toxic`). This filter level controls how aggressively to + * filter out potentially harmful content from responses. See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * and the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters | Responsible AI and usage guidelines} + * for more details. + * + * @public + */ +export type ImagenSafetyFilterLevel = + (typeof ImagenSafetyFilterLevel)[keyof typeof ImagenSafetyFilterLevel]; /** * A filter level controlling whether generation of images containing people or faces is allowed. @@ -138,13 +153,13 @@ export enum ImagenSafetyFilterLevel { * See the personGeneration * documentation for more details. * - * @beta + * @public */ -export enum ImagenPersonFilterLevel { +export const ImagenPersonFilterLevel = { /** * Disallow generation of images containing people or faces; images of people are filtered out. */ - BLOCK_ALL = 'dont_allow', + BLOCK_ALL: 'dont_allow', /** * Allow generation of images containing adults only; images of children are filtered out. * @@ -152,7 +167,7 @@ export enum ImagenPersonFilterLevel { * reviewed and approved by Cloud support; see the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen | Responsible AI and usage guidelines} * for more details. */ - ALLOW_ADULT = 'allow_adult', + ALLOW_ADULT: 'allow_adult', /** * Allow generation of images containing adults only; images of children are filtered out. * @@ -160,8 +175,19 @@ export enum ImagenPersonFilterLevel { * reviewed and approved by Cloud support; see the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen | Responsible AI and usage guidelines} * for more details. */ - ALLOW_ALL = 'allow_all' -} + ALLOW_ALL: 'allow_all' +} as const; + +/** + * A filter level controlling whether generation of images containing people or faces is allowed. + * + * See the personGeneration + * documentation for more details. + * + * @public + */ +export type ImagenPersonFilterLevel = + (typeof ImagenPersonFilterLevel)[keyof typeof ImagenPersonFilterLevel]; /** * Settings for controlling the aggressiveness of filtering out sensitive content. @@ -169,7 +195,7 @@ export enum ImagenPersonFilterLevel { * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } * for more details. * - * @beta + * @public */ export interface ImagenSafetySettings { /** @@ -189,30 +215,44 @@ export interface ImagenSafetySettings { * To specify an aspect ratio for generated images, set the `aspectRatio` property in your * {@link ImagenGenerationConfig}. * - * See the the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } * for more details and examples of the supported aspect ratios. * - * @beta + * @public */ -export enum ImagenAspectRatio { +export const ImagenAspectRatio = { /** * Square (1:1) aspect ratio. */ - SQUARE = '1:1', + 'SQUARE': '1:1', /** * Landscape (3:4) aspect ratio. */ - LANDSCAPE_3x4 = '3:4', + 'LANDSCAPE_3x4': '3:4', /** * Portrait (4:3) aspect ratio. */ - PORTRAIT_4x3 = '4:3', + 'PORTRAIT_4x3': '4:3', /** * Landscape (16:9) aspect ratio. */ - LANDSCAPE_16x9 = '16:9', + 'LANDSCAPE_16x9': '16:9', /** * Portrait (9:16) aspect ratio. */ - PORTRAIT_9x16 = '9:16' -} + 'PORTRAIT_9x16': '9:16' +} as const; + +/** + * Aspect ratios for Imagen images. + * + * To specify an aspect ratio for generated images, set the `aspectRatio` property in your + * {@link ImagenGenerationConfig}. + * + * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * for more details and examples of the supported aspect ratios. + * + * @public + */ +export type ImagenAspectRatio = + (typeof ImagenAspectRatio)[keyof typeof ImagenAspectRatio]; diff --git a/packages/ai/src/types/imagen/responses.ts b/packages/ai/src/types/imagen/responses.ts index 4b093fd550f..b0985ea6043 100644 --- a/packages/ai/src/types/imagen/responses.ts +++ b/packages/ai/src/types/imagen/responses.ts @@ -18,7 +18,7 @@ /** * An image generated by Imagen, represented as inline data. * - * @beta + * @public */ export interface ImagenInlineImage { /** @@ -37,6 +37,7 @@ export interface ImagenInlineImage { * An image generated by Imagen, stored in a Cloud Storage for Firebase bucket. * * This feature is not available yet. + * @public */ export interface ImagenGCSImage { /** @@ -56,7 +57,7 @@ export interface ImagenGCSImage { /** * The response from a request to generate images with Imagen. * - * @beta + * @public */ export interface ImagenGenerationResponse< T extends ImagenInlineImage | ImagenGCSImage @@ -72,8 +73,8 @@ export interface ImagenGenerationResponse< * The reason that images were filtered out. This property will only be defined if one * or more images were filtered. * - * Images may be filtered out due to the {@link ImagenSafetyFilterLevel}, - * {@link ImagenPersonFilterLevel}, or filtering included in the model. + * Images may be filtered out due to the {@link (ImagenSafetyFilterLevel:type)}, + * {@link (ImagenPersonFilterLevel:type)}, or filtering included in the model. * The filter levels may be adjusted in your {@link ImagenSafetySettings}. * * See the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen | Responsible AI and usage guidelines for Imagen} diff --git a/packages/ai/src/types/index.ts b/packages/ai/src/types/index.ts index 01f3e7a701a..2dfe73040ae 100644 --- a/packages/ai/src/types/index.ts +++ b/packages/ai/src/types/index.ts @@ -23,3 +23,15 @@ export * from './error'; export * from './schema'; export * from './imagen'; export * from './googleai'; +export { + LanguageModelCreateOptions, + LanguageModelCreateCoreOptions, + LanguageModelExpected, + LanguageModelMessage, + LanguageModelMessageContent, + LanguageModelMessageContentValue, + LanguageModelMessageRole, + LanguageModelMessageType, + LanguageModelPromptOptions +} from './language-model'; +export * from './chrome-adapter'; diff --git a/packages/ai/src/types/language-model.ts b/packages/ai/src/types/language-model.ts new file mode 100644 index 00000000000..9ac4c7202e1 --- /dev/null +++ b/packages/ai/src/types/language-model.ts @@ -0,0 +1,133 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * The subset of the Prompt API + * (see {@link https://github.com/webmachinelearning/prompt-api#full-api-surface-in-web-idl } + * required for hybrid functionality. + * + * @internal + */ +export interface LanguageModel extends EventTarget { + create(options?: LanguageModelCreateOptions): Promise; + availability(options?: LanguageModelCreateCoreOptions): Promise; + prompt( + input: LanguageModelPrompt, + options?: LanguageModelPromptOptions + ): Promise; + promptStreaming( + input: LanguageModelPrompt, + options?: LanguageModelPromptOptions + ): ReadableStream; + measureInputUsage( + input: LanguageModelPrompt, + options?: LanguageModelPromptOptions + ): Promise; + destroy(): undefined; +} + +/** + * @internal + */ +export enum Availability { + 'UNAVAILABLE' = 'unavailable', + 'DOWNLOADABLE' = 'downloadable', + 'DOWNLOADING' = 'downloading', + 'AVAILABLE' = 'available' +} + +/** + * Configures the creation of an on-device language model session. + * @beta + */ +export interface LanguageModelCreateCoreOptions { + topK?: number; + temperature?: number; + expectedInputs?: LanguageModelExpected[]; +} + +/** + * Configures the creation of an on-device language model session. + * @beta + */ +export interface LanguageModelCreateOptions + extends LanguageModelCreateCoreOptions { + signal?: AbortSignal; + initialPrompts?: LanguageModelMessage[]; +} + +/** + * Options for an on-device language model prompt. + * @beta + */ +export interface LanguageModelPromptOptions { + responseConstraint?: object; + // TODO: Restore AbortSignal once the API is defined. +} + +/** + * Options for the expected inputs for an on-device language model. + * @beta + */ export interface LanguageModelExpected { + type: LanguageModelMessageType; + languages?: string[]; +} + +/** + * An on-device language model prompt. + * @beta + */ +export type LanguageModelPrompt = LanguageModelMessage[]; + +/** + * An on-device language model message. + * @beta + */ +export interface LanguageModelMessage { + role: LanguageModelMessageRole; + content: LanguageModelMessageContent[]; +} + +/** + * An on-device language model content object. + * @beta + */ +export interface LanguageModelMessageContent { + type: LanguageModelMessageType; + value: LanguageModelMessageContentValue; +} + +/** + * Allowable roles for on-device language model usage. + * @beta + */ +export type LanguageModelMessageRole = 'system' | 'user' | 'assistant'; + +/** + * Allowable types for on-device language model messages. + * @beta + */ +export type LanguageModelMessageType = 'text' | 'image' | 'audio'; + +/** + * Content formats that can be provided as on-device message content. + * @beta + */ +export type LanguageModelMessageContentValue = + | ImageBitmapSource + | AudioBuffer + | BufferSource + | string; diff --git a/packages/ai/src/types/live-responses.ts b/packages/ai/src/types/live-responses.ts new file mode 100644 index 00000000000..d1870fa109f --- /dev/null +++ b/packages/ai/src/types/live-responses.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Content, + FunctionResponse, + GenerativeContentBlob, + Part +} from './content'; +import { LiveGenerationConfig, Tool, ToolConfig } from './requests'; + +/** + * User input that is sent to the model. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientContent { + clientContent: { + turns: [Content]; + turnComplete: boolean; + }; +} + +/** + * User input that is sent to the model in real time. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientRealtimeInput { + realtimeInput: { + mediaChunks: GenerativeContentBlob[]; + }; +} + +/** + * Function responses that are sent to the model in real time. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientToolResponse { + toolResponse: { + functionResponses: FunctionResponse[]; + }; +} + +/** + * The first message in a Live session, used to configure generation options. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientSetup { + setup: { + model: string; + generationConfig?: LiveGenerationConfig; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; + }; +} diff --git a/packages/ai/src/types/requests.ts b/packages/ai/src/types/requests.ts index 67f45095c2a..1e5fa367420 100644 --- a/packages/ai/src/types/requests.ts +++ b/packages/ai/src/types/requests.ts @@ -15,16 +15,21 @@ * limitations under the License. */ -import { TypedSchema } from '../requests/schema-builder'; +import { ObjectSchema, TypedSchema } from '../requests/schema-builder'; import { Content, Part } from './content'; +import { + LanguageModelCreateOptions, + LanguageModelPromptOptions +} from './language-model'; import { FunctionCallingMode, HarmBlockMethod, HarmBlockThreshold, HarmCategory, + InferenceMode, ResponseModality } from './enums'; -import { ObjectSchemaInterface, SchemaRequest } from './schema'; +import { ObjectSchemaRequest, SchemaRequest } from './schema'; /** * Base parameters for a number of methods. @@ -46,6 +51,17 @@ export interface ModelParams extends BaseParams { systemInstruction?: string | Part | Content; } +/** + * Params passed to {@link getLiveGenerativeModel}. + * @beta + */ +export interface LiveModelParams { + model: string; + generationConfig?: LiveGenerationConfig; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; +} /** * Request sent through {@link GenerativeModel.generateContent} * @public @@ -99,7 +115,7 @@ export interface GenerationConfig { * value can be a class generated with a {@link Schema} static method * like `Schema.string()` or `Schema.object()` or it can be a plain * JS object matching the {@link SchemaRequest} interface. - *
    Note: This only applies when the specified `responseMIMEType` supports a schema; currently + *
    Note: This only applies when the specified `responseMimeType` supports a schema; currently * this is limited to `application/json` and `text/x.enum`. */ responseSchema?: TypedSchema | SchemaRequest; @@ -113,6 +129,61 @@ export interface GenerationConfig { * @beta */ responseModalities?: ResponseModality[]; + /** + * Configuration for "thinking" behavior of compatible Gemini models. + */ + thinkingConfig?: ThinkingConfig; +} + +/** + * Configuration parameters used by {@link LiveGenerativeModel} to control live content generation. + * + * @beta + */ +export interface LiveGenerationConfig { + /** + * Configuration for speech synthesis. + */ + speechConfig?: SpeechConfig; + /** + * Specifies the maximum number of tokens that can be generated in the response. The number of + * tokens per word varies depending on the language outputted. Is unbounded by default. + */ + maxOutputTokens?: number; + /** + * Controls the degree of randomness in token selection. A `temperature` value of 0 means that the highest + * probability tokens are always selected. In this case, responses for a given prompt are mostly + * deterministic, but a small amount of variation is still possible. + */ + temperature?: number; + /** + * Changes how the model selects tokens for output. Tokens are + * selected from the most to least probable until the sum of their probabilities equals the `topP` + * value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively + * and the `topP` value is 0.5, then the model will select either A or B as the next token by using + * the `temperature` and exclude C as a candidate. Defaults to 0.95 if unset. + */ + topP?: number; + /** + * Changes how the model selects token for output. A `topK` value of 1 means the select token is + * the most probable among all tokens in the model's vocabulary, while a `topK` value 3 means that + * the next token is selected from among the 3 most probably using probabilities sampled. Tokens + * are then further filtered with the highest selected `temperature` sampling. Defaults to 40 + * if unspecified. + */ + topK?: number; + /** + * Positive penalties. + */ + presencePenalty?: number; + /** + * Frequency penalties. + */ + frequencyPenalty?: number; + /** + * The modalities of the response. + */ + responseModalities?: ResponseModality[]; } /** @@ -156,7 +227,10 @@ export interface RequestOptions { */ timeout?: number; /** - * Base url for endpoint. Defaults to https://firebasevertexai.googleapis.com + * Base url for endpoint. Defaults to + * https://firebasevertexai.googleapis.com, which is the + * {@link https://console.cloud.google.com/apis/library/firebasevertexai.googleapis.com?project=_ | Firebase AI Logic API} + * (used regardless of your chosen Gemini API provider). */ baseUrl?: string; } @@ -165,7 +239,11 @@ export interface RequestOptions { * Defines a tool that model can call to access external knowledge. * @public */ -export declare type Tool = FunctionDeclarationsTool; +export type Tool = + | FunctionDeclarationsTool + | GoogleSearchTool + | CodeExecutionTool + | URLContextTool; /** * Structured representation of a function declaration as defined by the @@ -176,7 +254,7 @@ export declare type Tool = FunctionDeclarationsTool; * as a Tool by the model and executed by the client. * @public */ -export declare interface FunctionDeclaration { +export interface FunctionDeclaration { /** * The name of the function to call. Must start with a letter or an * underscore. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with @@ -193,16 +271,83 @@ export declare interface FunctionDeclaration { * format. Reflects the Open API 3.03 Parameter Object. Parameter names are * case-sensitive. For a function with no parameters, this can be left unset. */ - parameters?: ObjectSchemaInterface; + parameters?: ObjectSchema | ObjectSchemaRequest; +} + +/** + * A tool that allows a Gemini model to connect to Google Search to access and incorporate + * up-to-date information from the web into its responses. + * + * Important: If using Grounding with Google Search, you are required to comply with the + * "Grounding with Google Search" usage requirements for your chosen API provider: {@link https://ai.google.dev/gemini-api/terms#grounding-with-google-search | Gemini Developer API} + * or Vertex AI Gemini API (see {@link https://cloud.google.com/terms/service-terms | Service Terms} + * section within the Service Specific Terms). + * + * @public + */ +export interface GoogleSearchTool { + /** + * Specifies the Google Search configuration. + * Currently, this is an empty object, but it's reserved for future configuration options. + * + * When using this feature, you are required to comply with the "Grounding with Google Search" + * usage requirements for your chosen API provider: {@link https://ai.google.dev/gemini-api/terms#grounding-with-google-search | Gemini Developer API} + * or Vertex AI Gemini API (see {@link https://cloud.google.com/terms/service-terms | Service Terms} + * section within the Service Specific Terms). + */ + googleSearch: GoogleSearch; +} + +/** + * A tool that enables the model to use code execution. + * + * @beta + */ +export interface CodeExecutionTool { + /** + * Specifies the Google Search configuration. + * Currently, this is an empty object, but it's reserved for future configuration options. + */ + codeExecution: {}; +} + +/** + * Specifies the Google Search configuration. + * + * @remarks Currently, this is an empty object, but it's reserved for future configuration options. + * + * @public + */ +export interface GoogleSearch {} + +/** + * A tool that allows you to provide additional context to the models in the form of public web + * URLs. By including URLs in your request, the Gemini model will access the content from those + * pages to inform and enhance its response. + * + * @beta + */ +export interface URLContextTool { + /** + * Specifies the URL Context configuration. + */ + urlContext: URLContext; } +/** + * Specifies the URL Context configuration. + * + * @beta + */ +export interface URLContext {} + /** * A `FunctionDeclarationsTool` is a piece of code that enables the system to * interact with external systems to perform an action, or set of actions, * outside of knowledge and scope of the model. * @public */ -export declare interface FunctionDeclarationsTool { +export interface FunctionDeclarationsTool { /** * Optional. One or more function declarations * to be passed to the model along with the current user query. Model may @@ -231,3 +376,105 @@ export interface FunctionCallingConfig { mode?: FunctionCallingMode; allowedFunctionNames?: string[]; } + +/** + * Encapsulates configuration for on-device inference. + * + * @beta + */ +export interface OnDeviceParams { + createOptions?: LanguageModelCreateOptions; + promptOptions?: LanguageModelPromptOptions; +} + +/** + * Configures hybrid inference. + * @beta + */ +export interface HybridParams { + /** + * Specifies on-device or in-cloud inference. Defaults to prefer on-device. + */ + mode: InferenceMode; + /** + * Optional. Specifies advanced params for on-device inference. + */ + onDeviceParams?: OnDeviceParams; + /** + * Optional. Specifies advanced params for in-cloud inference. + */ + inCloudParams?: ModelParams; +} + +/** + * Configuration for "thinking" behavior of compatible Gemini models. + * + * Certain models utilize a thinking process before generating a response. This allows them to + * reason through complex problems and plan a more coherent and accurate answer. + * + * @public + */ +export interface ThinkingConfig { + /** + * The thinking budget, in tokens. + * + * This parameter sets an upper limit on the number of tokens the model can use for its internal + * "thinking" process. A higher budget may result in higher quality responses for complex tasks + * but can also increase latency and cost. + * + * If you don't specify a budget, the model will determine the appropriate amount + * of thinking based on the complexity of the prompt. + * + * An error will be thrown if you set a thinking budget for a model that does not support this + * feature or if the specified budget is not within the model's supported range. + */ + thinkingBudget?: number; + + /** + * Whether to include "thought summaries" in the model's response. + * + * @remarks + * Thought summaries provide a brief overview of the model's internal thinking process, + * offering insight into how it arrived at the final answer. This can be useful for + * debugging, understanding the model's reasoning, and verifying its accuracy. + */ + includeThoughts?: boolean; +} + +/** + * Configuration for a pre-built voice. + * + * @beta + */ +export interface PrebuiltVoiceConfig { + /** + * The voice name to use for speech synthesis. + * + * For a full list of names and demos of what each voice sounds like, see {@link https://cloud.google.com/text-to-speech/docs/chirp3-hd | Chirp 3: HD Voices}. + */ + voiceName?: string; +} + +/** + * Configuration for the voice to used in speech synthesis. + * + * @beta + */ +export interface VoiceConfig { + /** + * Configures the voice using a pre-built voice configuration. + */ + prebuiltVoiceConfig?: PrebuiltVoiceConfig; +} + +/** + * Configures speech synthesis. + * + * @beta + */ +export interface SpeechConfig { + /** + * Configures the voice to be used in speech synthesis. + */ + voiceConfig?: VoiceConfig; +} diff --git a/packages/ai/src/types/responses.ts b/packages/ai/src/types/responses.ts index 71661f9feea..8b8e1351675 100644 --- a/packages/ai/src/types/responses.ts +++ b/packages/ai/src/types/responses.ts @@ -60,15 +60,34 @@ export interface EnhancedGenerateContentResponse */ text: () => string; /** - * Aggregates and returns all {@link InlineDataPart}s from the {@link GenerateContentResponse}'s - * first candidate. - * - * @returns An array of {@link InlineDataPart}s containing data from the response, if available. + * Aggregates and returns every {@link InlineDataPart} from the first candidate of + * {@link GenerateContentResponse}. * * @throws If the prompt or candidate was blocked. */ inlineDataParts: () => InlineDataPart[] | undefined; + /** + * Aggregates and returns every {@link FunctionCall} from the first candidate of + * {@link GenerateContentResponse}. + * + * @throws If the prompt or candidate was blocked. + */ functionCalls: () => FunctionCall[] | undefined; + /** + * Aggregates and returns every {@link TextPart} with their `thought` property set + * to `true` from the first candidate of {@link GenerateContentResponse}. + * + * @throws If the prompt or candidate was blocked. + * + * @remarks + * Thought summaries provide a brief overview of the model's internal thinking process, + * offering insight into how it arrived at the final answer. This can be useful for + * debugging, understanding the model's reasoning, and verifying its accuracy. + * + * Thoughts will only be included if {@link ThinkingConfig.includeThoughts} is + * set to `true`. + */ + thoughtSummary: () => string | undefined; } /** @@ -92,9 +111,21 @@ export interface GenerateContentResponse { export interface UsageMetadata { promptTokenCount: number; candidatesTokenCount: number; + /** + * The number of tokens used by the model's internal "thinking" process. + */ + thoughtsTokenCount?: number; totalTokenCount: number; + /** + * The number of tokens used by tools. + */ + toolUsePromptTokenCount?: number; promptTokensDetails?: ModalityTokenCount[]; candidatesTokensDetails?: ModalityTokenCount[]; + /** + * A list of tokens used by tools, broken down by modality. + */ + toolUsePromptTokensDetails?: ModalityTokenCount[]; } /** @@ -137,6 +168,7 @@ export interface GenerateContentCandidate { safetyRatings?: SafetyRating[]; citationMetadata?: CitationMetadata; groundingMetadata?: GroundingMetadata; + urlContextMetadata?: URLContextMetadata; } /** @@ -171,38 +203,249 @@ export interface Citation { } /** - * Metadata returned to client when grounding is enabled. + * Metadata returned when grounding is enabled. + * + * Currently, only Grounding with Google Search is supported (see {@link GoogleSearchTool}). + * + * Important: If using Grounding with Google Search, you are required to comply with the + * "Grounding with Google Search" usage requirements for your chosen API provider: {@link https://ai.google.dev/gemini-api/terms#grounding-with-google-search | Gemini Developer API} + * or Vertex AI Gemini API (see {@link https://cloud.google.com/terms/service-terms | Service Terms} + * section within the Service Specific Terms). + * * @public */ export interface GroundingMetadata { + /** + * Google Search entry point for web searches. This contains an HTML/CSS snippet that must be + * embedded in an app to display a Google Search entry point for follow-up web searches related to + * a model's "Grounded Response". + */ + searchEntryPoint?: SearchEntrypoint; + /** + * A list of {@link GroundingChunk} objects. Each chunk represents a piece of retrieved content + * (for example, from a web page). that the model used to ground its response. + */ + groundingChunks?: GroundingChunk[]; + /** + * A list of {@link GroundingSupport} objects. Each object details how specific segments of the + * model's response are supported by the `groundingChunks`. + */ + groundingSupports?: GroundingSupport[]; + /** + * A list of web search queries that the model performed to gather the grounding information. + * These can be used to allow users to explore the search results themselves. + */ webSearchQueries?: string[]; + /** + * @deprecated Use {@link GroundingSupport} instead. + */ retrievalQueries?: string[]; +} + +/** + * Google search entry point. + * + * @public + */ +export interface SearchEntrypoint { /** - * @deprecated + * HTML/CSS snippet that must be embedded in a web page. The snippet is designed to avoid + * undesired interaction with the rest of the page's CSS. + * + * To ensure proper rendering and prevent CSS conflicts, it is recommended + * to encapsulate this `renderedContent` within a shadow DOM when embedding it + * into a webpage. See {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM | MDN: Using shadow DOM}. + * + * @example + * ```javascript + * const container = document.createElement('div'); + * document.body.appendChild(container); + * container.attachShadow({ mode: 'open' }).innerHTML = renderedContent; + * ``` */ - groundingAttributions: GroundingAttribution[]; + renderedContent?: string; } /** - * @deprecated + * Represents a chunk of retrieved data that supports a claim in the model's response. This is part + * of the grounding information provided when grounding is enabled. + * * @public */ -export interface GroundingAttribution { - segment: Segment; - confidenceScore?: number; - web?: WebAttribution; - retrievedContext?: RetrievedContextAttribution; +export interface GroundingChunk { + /** + * Contains details if the grounding chunk is from a web source. + */ + web?: WebGroundingChunk; } /** + * A grounding chunk from the web. + * + * Important: If using Grounding with Google Search, you are required to comply with the + * {@link https://cloud.google.com/terms/service-terms | Service Specific Terms} for "Grounding with Google Search". + * + * @public + */ +export interface WebGroundingChunk { + /** + * The URI of the retrieved web page. + */ + uri?: string; + /** + * The title of the retrieved web page. + */ + title?: string; + /** + * The domain of the original URI from which the content was retrieved. + * + * This property is only supported in the Vertex AI Gemini API ({@link VertexAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property will be + * `undefined`. + */ + domain?: string; +} + +/** + * Provides information about how a specific segment of the model's response is supported by the + * retrieved grounding chunks. + * + * @public + */ +export interface GroundingSupport { + /** + * Specifies the segment of the model's response content that this grounding support pertains to. + */ + segment?: Segment; + /** + * A list of indices that refer to specific {@link GroundingChunk} objects within the + * {@link GroundingMetadata.groundingChunks} array. These referenced chunks + * are the sources that support the claim made in the associated `segment` of the response. + * For example, an array `[1, 3, 4]` means that `groundingChunks[1]`, `groundingChunks[3]`, + * and `groundingChunks[4]` are the retrieved content supporting this part of the response. + */ + groundingChunkIndices?: number[]; +} + +/** + * Represents a specific segment within a {@link Content} object, often used to + * pinpoint the exact location of text or data that grounding information refers to. + * * @public */ export interface Segment { + /** + * The zero-based index of the {@link Part} object within the `parts` array + * of its parent {@link Content} object. This identifies which part of the + * content the segment belongs to. + */ partIndex: number; + /** + * The zero-based start index of the segment within the specified `Part`, + * measured in UTF-8 bytes. This offset is inclusive, starting from 0 at the + * beginning of the part's content (e.g., `Part.text`). + */ startIndex: number; + /** + * The zero-based end index of the segment within the specified `Part`, + * measured in UTF-8 bytes. This offset is exclusive, meaning the character + * at this index is not included in the segment. + */ endIndex: number; + /** + * The text corresponding to the segment from the response. + */ + text: string; +} + +/** + * Metadata related to {@link URLContextTool}. + * + * @beta + */ +export interface URLContextMetadata { + /** + * List of URL metadata used to provide context to the Gemini model. + */ + urlMetadata: URLMetadata[]; } +/** + * Metadata for a single URL retrieved by the {@link URLContextTool} tool. + * + * @beta + */ +export interface URLMetadata { + /** + * The retrieved URL. + */ + retrievedUrl?: string; + /** + * The status of the URL retrieval. + */ + urlRetrievalStatus?: URLRetrievalStatus; +} + +/** + * The status of a URL retrieval. + * + * @remarks + * URL_RETRIEVAL_STATUS_UNSPECIFIED: Unspecified retrieval status. + *
    + * URL_RETRIEVAL_STATUS_SUCCESS: The URL retrieval was successful. + *
    + * URL_RETRIEVAL_STATUS_ERROR: The URL retrieval failed. + *
    + * URL_RETRIEVAL_STATUS_PAYWALL: The URL retrieval failed because the content is behind a paywall. + *
    + * URL_RETRIEVAL_STATUS_UNSAFE: The URL retrieval failed because the content is unsafe. + *
    + * + * @beta + */ +export const URLRetrievalStatus = { + /** + * Unspecified retrieval status. + */ + URL_RETRIEVAL_STATUS_UNSPECIFIED: 'URL_RETRIEVAL_STATUS_UNSPECIFIED', + /** + * The URL retrieval was successful. + */ + URL_RETRIEVAL_STATUS_SUCCESS: 'URL_RETRIEVAL_STATUS_SUCCESS', + /** + * The URL retrieval failed. + */ + URL_RETRIEVAL_STATUS_ERROR: 'URL_RETRIEVAL_STATUS_ERROR', + /** + * The URL retrieval failed because the content is behind a paywall. + */ + URL_RETRIEVAL_STATUS_PAYWALL: 'URL_RETRIEVAL_STATUS_PAYWALL', + /** + * The URL retrieval failed because the content is unsafe. + */ + URL_RETRIEVAL_STATUS_UNSAFE: 'URL_RETRIEVAL_STATUS_UNSAFE' +}; + +/** + * The status of a URL retrieval. + * + * @remarks + * URL_RETRIEVAL_STATUS_UNSPECIFIED: Unspecified retrieval status. + *
    + * URL_RETRIEVAL_STATUS_SUCCESS: The URL retrieval was successful. + *
    + * URL_RETRIEVAL_STATUS_ERROR: The URL retrieval failed. + *
    + * URL_RETRIEVAL_STATUS_PAYWALL: The URL retrieval failed because the content is behind a paywall. + *
    + * URL_RETRIEVAL_STATUS_UNSAFE: The URL retrieval failed because the content is unsafe. + *
    + * + * @beta + */ +export type URLRetrievalStatus = + (typeof URLRetrievalStatus)[keyof typeof URLRetrievalStatus]; + /** * @public */ @@ -274,9 +517,6 @@ export interface CountTokensResponse { * * The total number of billable characters counted across all instances * from the request. - * - * This property is only supported when using the Vertex AI Gemini API ({@link VertexAIBackend}). - * When using the Gemini Developer API ({@link GoogleAIBackend}), this property is not supported and will default to 0. */ totalBillableCharacters?: number; /** @@ -284,3 +524,73 @@ export interface CountTokensResponse { */ promptTokensDetails?: ModalityTokenCount[]; } + +/** + * An incremental content update from the model. + * + * @beta + */ +export interface LiveServerContent { + type: 'serverContent'; + /** + * The content that the model has generated as part of the current conversation with the user. + */ + modelTurn?: Content; + /** + * Indicates whether the turn is complete. This is `undefined` if the turn is not complete. + */ + turnComplete?: boolean; + /** + * Indicates whether the model was interrupted by the client. An interruption occurs when + * the client sends a message before the model finishes it's turn. This is `undefined` if the + * model was not interrupted. + */ + interrupted?: boolean; +} + +/** + * A request from the model for the client to execute one or more functions. + * + * @beta + */ +export interface LiveServerToolCall { + type: 'toolCall'; + /** + * An array of function calls to run. + */ + functionCalls: FunctionCall[]; +} + +/** + * Notification to cancel a previous function call triggered by {@link LiveServerToolCall}. + * + * @beta + */ +export interface LiveServerToolCallCancellation { + type: 'toolCallCancellation'; + /** + * IDs of function calls that were cancelled. These refer to the `id` property of a {@link FunctionCall}. + */ + functionIds: string[]; +} + +/** + * The types of responses that can be returned by {@link LiveSession.receive}. + * + * @beta + */ +export const LiveResponseType = { + SERVER_CONTENT: 'serverContent', + TOOL_CALL: 'toolCall', + TOOL_CALL_CANCELLATION: 'toolCallCancellation' +}; + +/** + * The types of responses that can be returned by {@link LiveSession.receive}. + * This is a property on all messages that can be used for type narrowing. This property is not + * returned by the server, it is assigned to a server message object once it's parsed. + * + * @beta + */ +export type LiveResponseType = + (typeof LiveResponseType)[keyof typeof LiveResponseType]; diff --git a/packages/ai/src/types/schema.ts b/packages/ai/src/types/schema.ts index 3a6c0c7301b..8068ce62a91 100644 --- a/packages/ai/src/types/schema.ts +++ b/packages/ai/src/types/schema.ts @@ -21,20 +21,28 @@ * {@link https://swagger.io/docs/specification/data-models/data-types/ | OpenAPI specification} * @public */ -export enum SchemaType { +export const SchemaType = { /** String type. */ - STRING = 'string', + STRING: 'string', /** Number type. */ - NUMBER = 'number', + NUMBER: 'number', /** Integer type. */ - INTEGER = 'integer', + INTEGER: 'integer', /** Boolean type. */ - BOOLEAN = 'boolean', + BOOLEAN: 'boolean', /** Array type. */ - ARRAY = 'array', + ARRAY: 'array', /** Object type. */ - OBJECT = 'object' -} + OBJECT: 'object' +} as const; + +/** + * Contains the list of OpenAPI data types + * as defined by the + * {@link https://swagger.io/docs/specification/data-models/data-types/ | OpenAPI specification} + * @public + */ +export type SchemaType = (typeof SchemaType)[keyof typeof SchemaType]; /** * Basic {@link Schema} properties shared across several Schema-related @@ -42,6 +50,12 @@ export enum SchemaType { * @public */ export interface SchemaShared { + /** + * An array of {@link Schema}. The generated data must be valid against any of the schemas + * listed in this array. This allows specifying multiple possible structures or types for a + * single field. + */ + anyOf?: T[]; /** Optional. The format of the property. * When using the Gemini Developer API ({@link GoogleAIBackend}), this must be either `'enum'` or * `'date-time'`, otherwise requests will fail. @@ -57,9 +71,9 @@ export interface SchemaShared { title?: string; /** Optional. The items of the property. */ items?: T; - /** The minimum number of items (elements) in a schema of type {@link SchemaType.ARRAY}. */ + /** The minimum number of items (elements) in a schema of {@link (SchemaType:type)} `array`. */ minItems?: number; - /** The maximum number of items (elements) in a schema of type {@link SchemaType.ARRAY}. */ + /** The maximum number of items (elements) in a schema of {@link (SchemaType:type)} `array`. */ maxItems?: number; /** Optional. Map of `Schema` objects. */ properties?: { @@ -93,10 +107,10 @@ export interface SchemaParams extends SchemaShared {} */ export interface SchemaRequest extends SchemaShared { /** - * The type of the property. {@link - * SchemaType}. + * The type of the property. this can only be undefined when using `anyOf` schemas, + * which do not have an explicit type in the {@link https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type | OpenAPI specification }. */ - type: SchemaType; + type?: SchemaType; /** Optional. Array of required property. */ required?: string[]; } @@ -107,17 +121,25 @@ export interface SchemaRequest extends SchemaShared { */ export interface SchemaInterface extends SchemaShared { /** - * The type of the property. {@link - * SchemaType}. + * The type of the property. this can only be undefined when using `anyof` schemas, + * which do not have an explicit type in the {@link https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type | OpenAPI Specification}. */ - type: SchemaType; + type?: SchemaType; } /** - * Interface for {@link ObjectSchema} class. + * Interface for JSON parameters in a schema of {@link (SchemaType:type)} + * "object" when not using the `Schema.object()` helper. * @public */ -export interface ObjectSchemaInterface extends SchemaInterface { - type: SchemaType.OBJECT; - optionalProperties?: string[]; +export interface ObjectSchemaRequest extends SchemaRequest { + type: 'object'; + /** + * This is not a property accepted in the final request to the backend, but is + * a client-side convenience property that is only usable by constructing + * a schema through the `Schema.object()` helper method. Populating this + * property will cause response errors if the object is not wrapped with + * `Schema.object()`. + */ + optionalProperties?: never; } diff --git a/packages/ai/src/websocket.test.ts b/packages/ai/src/websocket.test.ts new file mode 100644 index 00000000000..6d8f08282e7 --- /dev/null +++ b/packages/ai/src/websocket.test.ts @@ -0,0 +1,269 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinon, { SinonFakeTimers, SinonStub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { WebSocketHandlerImpl } from './websocket'; +import { AIError } from './errors'; + +use(sinonChai); +use(chaiAsPromised); + +class MockWebSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + readyState: number = MockWebSocket.CONNECTING; + sentMessages: Array = []; + url: string; + private listeners: Map> = new Map(); + + constructor(url: string) { + this.url = url; + } + + send(data: string | ArrayBuffer): void { + if (this.readyState !== MockWebSocket.OPEN) { + throw new Error('WebSocket is not in OPEN state'); + } + this.sentMessages.push(data); + } + + close(): void { + if ( + this.readyState === MockWebSocket.CLOSED || + this.readyState === MockWebSocket.CLOSING + ) { + return; + } + this.readyState = MockWebSocket.CLOSING; + setTimeout(() => { + this.readyState = MockWebSocket.CLOSED; + this.dispatchEvent(new Event('close')); + }, 10); + } + + addEventListener(type: string, listener: EventListener): void { + if (!this.listeners.has(type)) { + this.listeners.set(type, new Set()); + } + this.listeners.get(type)!.add(listener); + } + + removeEventListener(type: string, listener: EventListener): void { + this.listeners.get(type)?.delete(listener); + } + + dispatchEvent(event: Event): void { + this.listeners.get(event.type)?.forEach(listener => listener(event)); + } + + triggerOpen(): void { + this.readyState = MockWebSocket.OPEN; + this.dispatchEvent(new Event('open')); + } + + triggerMessage(data: any): void { + this.dispatchEvent(new MessageEvent('message', { data })); + } + + triggerError(): void { + this.dispatchEvent(new Event('error')); + } +} + +describe('WebSocketHandlerImpl', () => { + let handler: WebSocketHandlerImpl; + let mockWebSocket: MockWebSocket; + let clock: SinonFakeTimers; + let webSocketStub: SinonStub; + + beforeEach(() => { + webSocketStub = sinon + .stub(globalThis, 'WebSocket') + .callsFake((url: string) => { + mockWebSocket = new MockWebSocket(url); + return mockWebSocket as any; + }); + clock = sinon.useFakeTimers(); + handler = new WebSocketHandlerImpl(); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + describe('connect()', () => { + it('should resolve on open event', async () => { + const connectPromise = handler.connect('ws://test-url'); + expect(webSocketStub).to.have.been.calledWith('ws://test-url'); + + await clock.tickAsync(1); + mockWebSocket.triggerOpen(); + + await expect(connectPromise).to.be.fulfilled; + }); + + it('should reject on error event', async () => { + const connectPromise = handler.connect('ws://test-url'); + await clock.tickAsync(1); + mockWebSocket.triggerError(); + + await expect(connectPromise).to.be.rejectedWith( + AIError, + /Error event raised on WebSocket/ + ); + }); + }); + + describe('listen()', () => { + beforeEach(async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + }); + + it('should yield multiple messages as they arrive', async () => { + const generator = handler.listen(); + + const received: unknown[] = []; + const listenPromise = (async () => { + for await (const msg of generator) { + received.push(msg); + } + })(); + + // Use tickAsync to allow the consumer to start listening + await clock.tickAsync(1); + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 1 })])); + + await clock.tickAsync(10); + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 2 })])); + + await clock.tickAsync(5); + mockWebSocket.close(); + await clock.runAllAsync(); // Let timers finish + + await listenPromise; // Wait for the consumer to finish + + expect(received).to.deep.equal([ + { + foo: 1 + }, + { + foo: 2 + } + ]); + }); + + it('should buffer messages that arrive before the consumer calls .next()', async () => { + const generator = handler.listen(); + + // Create a promise that will consume the generator in a separate async context + const received: unknown[] = []; + const consumptionPromise = (async () => { + for await (const message of generator) { + received.push(message); + } + })(); + + await clock.tickAsync(1); + + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 1 })])); + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 2 })])); + + await clock.tickAsync(1); + mockWebSocket.close(); + await clock.runAllAsync(); + + await consumptionPromise; + + expect(received).to.deep.equal([ + { + foo: 1 + }, + { + foo: 2 + } + ]); + }); + }); + + describe('close()', () => { + it('should be idempotent and not throw if called multiple times', async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + + const closePromise1 = handler.close(); + await clock.runAllAsync(); + await closePromise1; + + await expect(handler.close()).to.be.fulfilled; + }); + + it('should wait for the onclose event before resolving', async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + + let closed = false; + const closePromise = handler.close().then(() => { + closed = true; + }); + + // The promise should not have resolved yet + await clock.tickAsync(5); + expect(closed).to.be.false; + + // Now, let the mock's setTimeout for closing run, which triggers onclose + await clock.tickAsync(10); + + await expect(closePromise).to.be.fulfilled; + expect(closed).to.be.true; + }); + }); + + describe('Interaction between listen() and close()', () => { + it('should allow close() to take precedence and resolve correctly, while also terminating the listener', async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + + const generator = handler.listen(); + const listenPromise = (async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _ of generator) { + } + })(); + + const closePromise = handler.close(); + + await clock.runAllAsync(); + + await expect(closePromise).to.be.fulfilled; + await expect(listenPromise).to.be.fulfilled; + + expect(mockWebSocket.readyState).to.equal(MockWebSocket.CLOSED); + }); + }); +}); diff --git a/packages/ai/src/websocket.ts b/packages/ai/src/websocket.ts new file mode 100644 index 00000000000..fa34f2d48c3 --- /dev/null +++ b/packages/ai/src/websocket.ts @@ -0,0 +1,241 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from './errors'; +import { logger } from './logger'; +import { AIErrorCode } from './types'; + +/** + * A standardized interface for interacting with a WebSocket connection. + * This abstraction allows the SDK to use the appropriate WebSocket implementation + * for the current JS environment (Browser vs. Node) without + * changing the core logic of the `LiveSession`. + * @internal + */ + +export interface WebSocketHandler { + /** + * Establishes a connection to the given URL. + * + * @param url The WebSocket URL (e.g., wss://...). + * @returns A promise that resolves on successful connection or rejects on failure. + */ + connect(url: string): Promise; + + /** + * Sends data over the WebSocket. + * + * @param data The string or binary data to send. + */ + send(data: string | ArrayBuffer): void; + + /** + * Returns an async generator that yields parsed JSON objects from the server. + * The yielded type is `unknown` because the handler cannot guarantee the shape of the data. + * The consumer is responsible for type validation. + * The generator terminates when the connection is closed. + * + * @returns A generator that allows consumers to pull messages using a `for await...of` loop. + */ + listen(): AsyncGenerator; + + /** + * Closes the WebSocket connection. + * + * @param code - A numeric status code explaining why the connection is closing. + * @param reason - A human-readable string explaining why the connection is closing. + */ + close(code?: number, reason?: string): Promise; +} + +/** + * A wrapper for the native `WebSocket` available in both Browsers and Node >= 22. + * + * @internal + */ +export class WebSocketHandlerImpl implements WebSocketHandler { + private ws?: WebSocket; + + constructor() { + if (typeof WebSocket === 'undefined') { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'The WebSocket API is not available in this environment. ' + + 'The "Live" feature is not supported here. It is supported in ' + + 'modern browser windows, Web Workers with WebSocket support, and Node >= 22.' + ); + } + } + + connect(url: string): Promise { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(url); + this.ws.binaryType = 'blob'; // Only important to set in Node + this.ws.addEventListener('open', () => resolve(), { once: true }); + this.ws.addEventListener( + 'error', + () => + reject( + new AIError( + AIErrorCode.FETCH_ERROR, + `Error event raised on WebSocket` + ) + ), + { once: true } + ); + this.ws!.addEventListener('close', (closeEvent: CloseEvent) => { + if (closeEvent.reason) { + logger.warn( + `WebSocket connection closed by server. Reason: '${closeEvent.reason}'` + ); + } + }); + }); + } + + send(data: string | ArrayBuffer): void { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'WebSocket is not open.'); + } + this.ws.send(data); + } + + async *listen(): AsyncGenerator { + if (!this.ws) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'WebSocket is not connected.' + ); + } + + const messageQueue: unknown[] = []; + const errorQueue: Error[] = []; + let resolvePromise: (() => void) | null = null; + let isClosed = false; + + const messageListener = async (event: MessageEvent): Promise => { + let data: string; + if (event.data instanceof Blob) { + data = await event.data.text(); + } else if (typeof event.data === 'string') { + data = event.data; + } else { + errorQueue.push( + new AIError( + AIErrorCode.PARSE_FAILED, + `Failed to parse WebSocket response. Expected data to be a Blob or string, but was ${typeof event.data}.` + ) + ); + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + return; + } + + try { + const obj = JSON.parse(data) as unknown; + messageQueue.push(obj); + } catch (e) { + const err = e as Error; + errorQueue.push( + new AIError( + AIErrorCode.PARSE_FAILED, + `Error parsing WebSocket message to JSON: ${err.message}` + ) + ); + } + + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + }; + + const errorListener = (): void => { + errorQueue.push( + new AIError(AIErrorCode.FETCH_ERROR, 'WebSocket connection error.') + ); + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + }; + + const closeListener = (event: CloseEvent): void => { + if (event.reason) { + logger.warn( + `WebSocket connection closed by the server with reason: ${event.reason}` + ); + } + isClosed = true; + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + // Clean up listeners to prevent memory leaks + this.ws?.removeEventListener('message', messageListener); + this.ws?.removeEventListener('close', closeListener); + this.ws?.removeEventListener('error', errorListener); + }; + + this.ws.addEventListener('message', messageListener); + this.ws.addEventListener('close', closeListener); + this.ws.addEventListener('error', errorListener); + + while (!isClosed) { + if (errorQueue.length > 0) { + const error = errorQueue.shift()!; + throw error; + } + if (messageQueue.length > 0) { + yield messageQueue.shift()!; + } else { + await new Promise(resolve => { + resolvePromise = resolve; + }); + } + } + + // If the loop terminated because isClosed is true, check for any final errors + if (errorQueue.length > 0) { + const error = errorQueue.shift()!; + throw error; + } + } + + close(code?: number, reason?: string): Promise { + return new Promise(resolve => { + if (!this.ws) { + return resolve(); + } + + this.ws.addEventListener('close', () => resolve(), { once: true }); + // Calling 'close' during these states results in an error. + if ( + this.ws.readyState === WebSocket.CLOSED || + this.ws.readyState === WebSocket.CONNECTING + ) { + return resolve(); + } + + if (this.ws.readyState !== WebSocket.CLOSING) { + this.ws.close(code, reason); + } + }); + } +} diff --git a/packages/ai/test-utils/convert-mocks.ts b/packages/ai/test-utils/convert-mocks.ts index 4bac70d1d10..34233a73ace 100644 --- a/packages/ai/test-utils/convert-mocks.ts +++ b/packages/ai/test-utils/convert-mocks.ts @@ -19,6 +19,8 @@ const { readdirSync, readFileSync, writeFileSync } = require('node:fs'); const { join } = require('node:path'); +type BackendName = import('./types').BackendName; // Import type without triggering ES module detection + const MOCK_RESPONSES_DIR_PATH = join( __dirname, 'vertexai-sdk-test-data', @@ -26,8 +28,6 @@ const MOCK_RESPONSES_DIR_PATH = join( ); const MOCK_LOOKUP_OUTPUT_PATH = join(__dirname, 'mocks-lookup.ts'); -type BackendName = 'vertexAI' | 'googleAI'; - const mockDirs: Record = { vertexAI: join(MOCK_RESPONSES_DIR_PATH, 'vertexai'), googleAI: join(MOCK_RESPONSES_DIR_PATH, 'googleai') diff --git a/packages/ai/test-utils/get-fake-firebase-services.ts b/packages/ai/test-utils/get-fake-firebase-services.ts new file mode 100644 index 00000000000..63789c1a00e --- /dev/null +++ b/packages/ai/test-utils/get-fake-firebase-services.ts @@ -0,0 +1,71 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + FirebaseApp, + initializeApp, + _registerComponent, + _addOrOverwriteComponent +} from '@firebase/app'; +import { Component, ComponentType } from '@firebase/component'; +import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types'; +import { AI_TYPE } from '../src/constants'; +import { factory } from '../src/factory-browser'; + +const fakeConfig = { + projectId: 'projectId', + authDomain: 'authDomain', + messagingSenderId: 'messagingSenderId', + databaseURL: 'databaseUrl', + storageBucket: 'storageBucket' +}; + +export function getFullApp(fakeAppParams?: { + appId?: string; + apiKey?: string; +}): FirebaseApp { + _registerComponent( + new Component(AI_TYPE, factory, ComponentType.PUBLIC).setMultipleInstances( + true + ) + ); + _registerComponent( + new Component( + 'app-check-internal', + () => { + return {} as FirebaseAppCheckInternal; + }, + ComponentType.PUBLIC + ) + ); + const app = initializeApp({ ...fakeConfig, ...fakeAppParams }); + _addOrOverwriteComponent( + app, + //@ts-ignore + new Component( + 'heartbeat', + // @ts-ignore + () => { + return { + triggerHeartbeat: () => {} + }; + }, + ComponentType.PUBLIC + ) + ); + return app; +} diff --git a/packages/ai/test-utils/mock-response.ts b/packages/ai/test-utils/mock-response.ts index 5128ddabe74..4963bcbb193 100644 --- a/packages/ai/test-utils/mock-response.ts +++ b/packages/ai/test-utils/mock-response.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { BackendName } from './types'; import { vertexAIMocksLookup, googleAIMocksLookup } from './mocks-lookup'; const mockSetMaps: Record> = { diff --git a/packages/firebase/vertexai/index.ts b/packages/ai/test-utils/types.ts similarity index 87% rename from packages/firebase/vertexai/index.ts rename to packages/ai/test-utils/types.ts index 530f99162ed..00b99eef55a 100644 --- a/packages/firebase/vertexai/index.ts +++ b/packages/ai/test-utils/types.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2024 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,4 +15,4 @@ * limitations under the License. */ -export * from '@firebase/ai'; +export type BackendName = 'vertexAI' | 'googleAI'; diff --git a/packages/analytics-compat/CHANGELOG.md b/packages/analytics-compat/CHANGELOG.md index ce440211b4a..f2d4d39b618 100644 --- a/packages/analytics-compat/CHANGELOG.md +++ b/packages/analytics-compat/CHANGELOG.md @@ -1,5 +1,25 @@ # @firebase/analytics-compat +## 0.2.24 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/analytics@0.10.18 + - @firebase/component@0.7.0 + - @firebase/util@1.13.0 + +## 0.2.23 + +### Patch Changes + +- Updated dependencies [[`13e6cce`](https://github.com/firebase/firebase-js-sdk/commit/13e6cce882d687e06c8d9bfb56895f8a77fc57b5), [`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/analytics@0.10.17 + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 0.2.22 ### Patch Changes diff --git a/packages/analytics-compat/package.json b/packages/analytics-compat/package.json index d126569486a..c978d3653cf 100644 --- a/packages/analytics-compat/package.json +++ b/packages/analytics-compat/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/analytics-compat", - "version": "0.2.22", + "version": "0.2.24", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -22,7 +22,7 @@ "@firebase/app-compat": "0.x" }, "devDependencies": { - "@firebase/app-compat": "0.4.1", + "@firebase/app-compat": "0.5.3", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", @@ -52,10 +52,10 @@ }, "typings": "dist/src/index.d.ts", "dependencies": { - "@firebase/component": "0.6.17", - "@firebase/analytics": "0.10.16", + "@firebase/component": "0.7.0", + "@firebase/analytics": "0.10.18", "@firebase/analytics-types": "0.8.3", - "@firebase/util": "1.12.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "nyc": { diff --git a/packages/analytics-interop-types/index.d.ts b/packages/analytics-interop-types/index.d.ts index b3e2fb0fe07..6cb6936147b 100644 --- a/packages/analytics-interop-types/index.d.ts +++ b/packages/analytics-interop-types/index.d.ts @@ -29,6 +29,10 @@ export interface FirebaseAnalyticsInternal { eventParams?: { [key: string]: unknown }, options?: AnalyticsCallOptions ): void; + setUserProperties: ( + properties: { [key: string]: unknown }, + options?: AnalyticsCallOptions + ) => void; } export interface AnalyticsCallOptions { diff --git a/packages/analytics/CHANGELOG.md b/packages/analytics/CHANGELOG.md index 2507a977522..ccb9017314a 100644 --- a/packages/analytics/CHANGELOG.md +++ b/packages/analytics/CHANGELOG.md @@ -1,5 +1,28 @@ # @firebase/analytics +## 0.10.18 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/installations@0.6.19 + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.10.17 + +### Patch Changes + +- [`13e6cce`](https://github.com/firebase/firebase-js-sdk/commit/13e6cce882d687e06c8d9bfb56895f8a77fc57b5) [#9085](https://github.com/firebase/firebase-js-sdk/pull/9085) - Add rollup config to generate modular typings for google3 + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/installations@0.6.18 + ## 0.10.16 ### Patch Changes diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 5d4c4d06735..c8d359102f0 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/analytics", - "version": "0.10.16", + "version": "0.10.18", "description": "A analytics package for new firebase packages", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/analytics-public.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -39,15 +39,15 @@ "@firebase/app": "0.x" }, "dependencies": { - "@firebase/installations": "0.6.17", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "rollup-plugin-dts": "5.3.1", "@rollup/plugin-commonjs": "21.1.0", diff --git a/packages/analytics/rollup.config.js b/packages/analytics/rollup.config.js index 529858f147f..f119da4bd5f 100644 --- a/packages/analytics/rollup.config.js +++ b/packages/analytics/rollup.config.js @@ -53,7 +53,7 @@ const esmBuilds = [ external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ] } @@ -73,7 +73,7 @@ const cjsBuilds = [ external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ] } ]; diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts index f79725db7bd..6017aed21b0 100644 --- a/packages/analytics/src/index.ts +++ b/packages/analytics/src/index.ts @@ -33,9 +33,9 @@ import { InstanceFactoryOptions } from '@firebase/component'; import { ERROR_FACTORY, AnalyticsError } from './errors'; -import { logEvent } from './api'; +import { logEvent, setUserProperties } from './api'; import { name, version } from '../package.json'; -import { AnalyticsCallOptions } from './public-types'; +import { AnalyticsCallOptions, CustomParams } from './public-types'; import '@firebase/installations'; declare global { @@ -66,7 +66,7 @@ function registerAnalytics(): void { ); registerVersion(name, version); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); function internalFactory( @@ -79,7 +79,11 @@ function registerAnalytics(): void { eventName: string, eventParams?: { [key: string]: unknown }, options?: AnalyticsCallOptions - ) => logEvent(analytics, eventName, eventParams, options) + ) => logEvent(analytics, eventName, eventParams, options), + setUserProperties: ( + properties: CustomParams, + options?: AnalyticsCallOptions + ) => setUserProperties(analytics, properties, options) }; } catch (e) { throw ERROR_FACTORY.create(AnalyticsError.INTEROP_COMPONENT_REG_FAILED, { diff --git a/packages/app-check-compat/CHANGELOG.md b/packages/app-check-compat/CHANGELOG.md index b5456ba7b69..8d8cca1a8d3 100644 --- a/packages/app-check-compat/CHANGELOG.md +++ b/packages/app-check-compat/CHANGELOG.md @@ -1,5 +1,30 @@ # @firebase/app-check-compat +## 0.4.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/app-check@0.11.0 + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.3.26 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/app-check@0.10.1 + - @firebase/component@0.6.18 + ## 0.3.25 ### Patch Changes diff --git a/packages/app-check-compat/package.json b/packages/app-check-compat/package.json index 29ed9977205..d2b13f78d39 100644 --- a/packages/app-check-compat/package.json +++ b/packages/app-check-compat/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/app-check-compat", - "version": "0.3.25", + "version": "0.4.0", "description": "A compat App Check package for new firebase packages", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -34,16 +34,16 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/app-check": "0.10.0", + "@firebase/app-check": "0.11.0", "@firebase/app-check-types": "0.5.3", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app-compat": "0.4.1", + "@firebase/app-compat": "0.5.3", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", @@ -67,6 +67,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/app-check/.changeset/chatty-laws-sleep.md b/packages/app-check/.changeset/chatty-laws-sleep.md new file mode 100644 index 00000000000..634ba952c67 --- /dev/null +++ b/packages/app-check/.changeset/chatty-laws-sleep.md @@ -0,0 +1,5 @@ +--- +'@firebase/app-check': patch +--- + +Prevent redundant exchangeToken calls in debug mode diff --git a/packages/app-check/CHANGELOG.md b/packages/app-check/CHANGELOG.md index 764c17928cb..e28884ed1b4 100644 --- a/packages/app-check/CHANGELOG.md +++ b/packages/app-check/CHANGELOG.md @@ -1,5 +1,28 @@ # @firebase/app-check +## 0.11.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.10.1 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 0.10.0 ### Minor Changes diff --git a/packages/app-check/package.json b/packages/app-check/package.json index e52ff90dbc7..a0e03d895cd 100644 --- a/packages/app-check/package.json +++ b/packages/app-check/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/app-check", - "version": "0.10.0", + "version": "0.11.0", "description": "The App Check component of the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/app-check-public.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -37,14 +37,14 @@ "@firebase/app": "0.x" }, "dependencies": { - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", - "@firebase/logger": "0.4.4", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", "tslib": "^2.1.0" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", @@ -68,6 +68,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/app-check/src/internal-api.test.ts b/packages/app-check/src/internal-api.test.ts index 5d6b88f1c32..ce89430a15b 100644 --- a/packages/app-check/src/internal-api.test.ts +++ b/packages/app-check/src/internal-api.test.ts @@ -630,6 +630,31 @@ describe('internal api', () => { expect(token).to.deep.equal({ token: fakeRecaptchaAppCheckToken.token }); }); + it('exchanges debug token only once if debug mode with no cached token', async () => { + const exchangeTokenStub: SinonStub = stub( + client, + 'exchangeToken' + ).returns(Promise.resolve(fakeRecaptchaAppCheckToken)); + const debugState = getDebugState(); + debugState.enabled = true; + debugState.token = new Deferred(); + debugState.token.resolve('my-debug-token'); + const appCheck = initializeAppCheck(app, { + provider: new ReCaptchaV3Provider(FAKE_SITE_KEY) + }); + const appCheckService = appCheck as AppCheckService; + const [token1, token2] = await Promise.all([ + getToken(appCheckService), + getToken(appCheckService) + ]); + expect(exchangeTokenStub.args[0][0].body['debug_token']).to.equal( + 'my-debug-token' + ); + expect(token1).to.deep.equal({ token: fakeRecaptchaAppCheckToken.token }); + expect(token2).to.deep.equal({ token: fakeRecaptchaAppCheckToken.token }); + expect(exchangeTokenStub).to.be.calledOnce; + }); + it('throttles for a period less than 1d on 503', async () => { // More detailed check of exponential backoff in providers.test.ts const appCheck = initializeAppCheck(app, { diff --git a/packages/app-check/src/internal-api.ts b/packages/app-check/src/internal-api.ts index eddf043c843..158c723a057 100644 --- a/packages/app-check/src/internal-api.ts +++ b/packages/app-check/src/internal-api.ts @@ -118,10 +118,11 @@ export async function getToken( */ if (isDebugMode()) { try { + const debugToken = await getDebugToken(); // Avoid making another call to the exchange endpoint if one is in flight. if (!state.exchangeTokenPromise) { state.exchangeTokenPromise = exchangeToken( - getExchangeDebugTokenRequest(app, await getDebugToken()), + getExchangeDebugTokenRequest(app, debugToken), appCheck.heartbeatServiceProvider ).finally(() => { // Clear promise when settled - either resolved or rejected. diff --git a/packages/app-compat/CHANGELOG.md b/packages/app-compat/CHANGELOG.md index bc8b1b97982..adcd3b62a59 100644 --- a/packages/app-compat/CHANGELOG.md +++ b/packages/app-compat/CHANGELOG.md @@ -1,5 +1,51 @@ # @firebase/app-compat +## 0.5.3 + +### Patch Changes + +- Updated dependencies []: + - @firebase/app@0.14.3 + +## 0.5.2 + +### Patch Changes + +- Updated dependencies []: + - @firebase/app@0.14.2 + +## 0.5.1 + +### Patch Changes + +- Updated dependencies []: + - @firebase/app@0.14.1 + +## 0.5.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`d91169f`](https://github.com/firebase/firebase-js-sdk/commit/d91169f061bf1dcbfe78a8c8a7f739677608fcb7), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + - @firebase/app@0.14.0 + +## 0.4.2 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83), [`bb57947`](https://github.com/firebase/firebase-js-sdk/commit/bb57947c942e44b39e5b0254324bee6bf665fd4e)]: + - @firebase/util@1.12.1 + - @firebase/app@0.13.2 + - @firebase/component@0.6.18 + ## 0.4.1 ### Patch Changes diff --git a/packages/app-compat/package.json b/packages/app-compat/package.json index 8dba6ff2ff6..a27c1627811 100644 --- a/packages/app-compat/package.json +++ b/packages/app-compat/package.json @@ -1,18 +1,18 @@ { "name": "@firebase/app-compat", - "version": "0.4.1", + "version": "0.5.3", "description": "The primary entrypoint to the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "lite": "dist/index.lite.js", "exports": { ".": { "types": "./dist/app-compat-public.d.ts", "require": "./dist/index.cjs.js", "lite": "./dist/index.lite.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -37,10 +37,10 @@ }, "license": "Apache-2.0", "dependencies": { - "@firebase/app": "0.13.1", - "@firebase/util": "1.12.0", - "@firebase/logger": "0.4.4", - "@firebase/component": "0.6.17", + "@firebase/app": "0.14.3", + "@firebase/util": "1.13.0", + "@firebase/logger": "0.5.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" }, "devDependencies": { @@ -66,6 +66,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index aa13db67c63..5e283b1bef0 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,50 @@ # @firebase/app +## 0.14.3 + +### Patch Changes + +- Update SDK_VERSION. + +## 0.14.2 + +### Patch Changes + +- Update SDK_VERSION. + +## 0.14.1 + +### Patch Changes + +- Update SDK_VERSION. + +## 0.14.0 + +### Minor Changes + +- [`d91169f`](https://github.com/firebase/firebase-js-sdk/commit/d91169f061bf1dcbfe78a8c8a7f739677608fcb7) [#9151](https://github.com/firebase/firebase-js-sdk/pull/9151) (fixes [#8863](https://github.com/firebase/firebase-js-sdk/issues/8863)) - initializeServerApp now supports auto-initialization for Firebase App Hosting. + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.13.2 + +### Patch Changes + +- [`bb57947`](https://github.com/firebase/firebase-js-sdk/commit/bb57947c942e44b39e5b0254324bee6bf665fd4e) [#9112](https://github.com/firebase/firebase-js-sdk/pull/9112) (fixes [#8988](https://github.com/firebase/firebase-js-sdk/issues/8988)) - Add "react-native" entry point to @firebase/app + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 0.13.1 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 3d6a0ca5dca..9d9d9d3d89e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,18 +1,18 @@ { "name": "@firebase/app", - "version": "0.13.1", + "version": "0.14.3", "description": "The primary entrypoint to the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "react-native": "dist/index.cjs.js", "exports": { ".": { "types": "./dist/app-public.d.ts", "require": "./dist/index.cjs.js", "react-native": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -39,9 +39,9 @@ "typings:internal": "node ../../scripts/build/use_typings.js ./dist/app.d.ts" }, "dependencies": { - "@firebase/util": "1.12.0", - "@firebase/logger": "0.4.4", - "@firebase/component": "0.6.17", + "@firebase/util": "1.13.0", + "@firebase/logger": "0.5.0", + "@firebase/component": "0.7.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -70,6 +70,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/app/rollup.config.js b/packages/app/rollup.config.js index 8314734a746..0e916a43b26 100644 --- a/packages/app/rollup.config.js +++ b/packages/app/rollup.config.js @@ -54,7 +54,7 @@ const esmBuilds = [ plugins: [ ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('esm', 2017), + ...generateBuildTargetReplaceConfig('esm', 2020), '__RUNTIME_ENV__': '' }), emitModulePackageFile() @@ -74,7 +74,7 @@ const cjsBuilds = [ plugins: [ ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('cjs', 2017), + ...generateBuildTargetReplaceConfig('cjs', 2020), '__RUNTIME_ENV__': 'node' }) ] diff --git a/packages/app/src/api.ts b/packages/app/src/api.ts index 9cba8ec6f50..af9b401b57d 100644 --- a/packages/app/src/api.ts +++ b/packages/app/src/api.ts @@ -37,6 +37,7 @@ import { _apps, _components, _isFirebaseApp, + _isFirebaseServerAppSettings, _registerComponent, _serverApps } from './internal'; @@ -106,6 +107,10 @@ export const SDK_VERSION = version; * * @returns The initialized app. * + * @throws If the optional `name` parameter is malformed or empty. + * + * @throws If a `FirebaseApp` already exists with the same name but with a different configuration. + * * @public */ export function initializeApp( @@ -118,6 +123,9 @@ export function initializeApp( * @param options - Options to configure the app's services. * @param config - FirebaseApp Configuration * + * @throws If {@link FirebaseAppSettings.name} is defined but the value is malformed or empty. + * + * @throws If a `FirebaseApp` already exists with the same name but with a different configuration. * @public */ export function initializeApp( @@ -220,41 +228,75 @@ export function initializeApp( * * @param options - `Firebase.AppOptions` to configure the app's services, or a * a `FirebaseApp` instance which contains the `AppOptions` within. - * @param config - `FirebaseServerApp` configuration. + * @param config - Optional `FirebaseServerApp` settings. * * @returns The initialized `FirebaseServerApp`. * + * @throws If invoked in an unsupported non-server environment such as a browser. + * + * @throws If {@link FirebaseServerAppSettings.releaseOnDeref} is defined but the runtime doesn't + * provide Finalization Registry support. + * * @public */ export function initializeServerApp( options: FirebaseOptions | FirebaseApp, - config: FirebaseServerAppSettings + config?: FirebaseServerAppSettings ): FirebaseServerApp; +/** + * Creates and initializes a {@link @firebase/app#FirebaseServerApp} instance. + * + * @param config - Optional `FirebaseServerApp` settings. + * + * @returns The initialized `FirebaseServerApp`. + * + * @throws If invoked in an unsupported non-server environment such as a browser. + * @throws If {@link FirebaseServerAppSettings.releaseOnDeref} is defined but the runtime doesn't + * provide Finalization Registry support. + * @throws If the `FIREBASE_OPTIONS` environment variable does not contain a valid project + * configuration required for auto-initialization. + * + * @public + */ export function initializeServerApp( - _options: FirebaseOptions | FirebaseApp, - _serverAppConfig: FirebaseServerAppSettings + config?: FirebaseServerAppSettings +): FirebaseServerApp; +export function initializeServerApp( + _options?: FirebaseApp | FirebaseServerAppSettings | FirebaseOptions, + _serverAppConfig: FirebaseServerAppSettings = {} ): FirebaseServerApp { if (isBrowser() && !isWebWorker()) { // FirebaseServerApp isn't designed to be run in browsers. throw ERROR_FACTORY.create(AppError.INVALID_SERVER_APP_ENVIRONMENT); } - if (_serverAppConfig.automaticDataCollectionEnabled === undefined) { - _serverAppConfig.automaticDataCollectionEnabled = true; + let firebaseOptions: FirebaseOptions | undefined; + let serverAppSettings: FirebaseServerAppSettings = _serverAppConfig || {}; + + if (_options) { + if (_isFirebaseApp(_options)) { + firebaseOptions = _options.options; + } else if (_isFirebaseServerAppSettings(_options)) { + serverAppSettings = _options; + } else { + firebaseOptions = _options; + } } - let appOptions: FirebaseOptions; - if (_isFirebaseApp(_options)) { - appOptions = _options.options; - } else { - appOptions = _options; + if (serverAppSettings.automaticDataCollectionEnabled === undefined) { + serverAppSettings.automaticDataCollectionEnabled = true; + } + + firebaseOptions ||= getDefaultAppConfig(); + if (!firebaseOptions) { + throw ERROR_FACTORY.create(AppError.NO_OPTIONS); } // Build an app name based on a hash of the configuration options. const nameObj = { - ..._serverAppConfig, - ...appOptions + ...serverAppSettings, + ...firebaseOptions }; // However, Do not mangle the name based on releaseOnDeref, since it will vary between the @@ -270,7 +312,7 @@ export function initializeServerApp( ); }; - if (_serverAppConfig.releaseOnDeref !== undefined) { + if (serverAppSettings.releaseOnDeref !== undefined) { if (typeof FinalizationRegistry === 'undefined') { throw ERROR_FACTORY.create( AppError.FINALIZATION_REGISTRY_NOT_SUPPORTED, @@ -283,7 +325,7 @@ export function initializeServerApp( const existingApp = _serverApps.get(nameString) as FirebaseServerApp; if (existingApp) { (existingApp as FirebaseServerAppImpl).incRefCount( - _serverAppConfig.releaseOnDeref + serverAppSettings.releaseOnDeref ); return existingApp; } @@ -294,8 +336,8 @@ export function initializeServerApp( } const newApp = new FirebaseServerAppImpl( - appOptions, - _serverAppConfig, + firebaseOptions, + serverAppSettings, nameString, container ); diff --git a/packages/app/src/heartbeatService.test.ts b/packages/app/src/heartbeatService.test.ts index 57f97ec7468..caebba457c3 100644 --- a/packages/app/src/heartbeatService.test.ts +++ b/packages/app/src/heartbeatService.test.ts @@ -149,8 +149,10 @@ describe('HeartbeatServiceImpl', () => { expect(emptyHeaders).to.equal(''); }); it(`triggerHeartbeat() doesn't throw even if code errors`, async () => { - //@ts-expect-error Ensure this doesn't match - heartbeatService._heartbeatsCache?.lastSentHeartbeatDate = 50; + if (heartbeatService._heartbeatsCache) { + //@ts-expect-error Ensure this doesn't match + heartbeatService._heartbeatsCache.lastSentHeartbeatDate = 50; + } //@ts-expect-error Ensure you can't .push() to this heartbeatService._heartbeatsCache.heartbeats = 50; const warnStub = stub(console, 'warn'); diff --git a/packages/app/src/internal.ts b/packages/app/src/internal.ts index cbcdcb26501..422c941aba3 100644 --- a/packages/app/src/internal.ts +++ b/packages/app/src/internal.ts @@ -17,6 +17,8 @@ import { FirebaseApp, + FirebaseAppSettings, + FirebaseServerAppSettings, FirebaseOptions, FirebaseServerApp } from './public-types'; @@ -147,18 +149,40 @@ export function _removeServiceInstance( /** * - * @param obj - an object of type FirebaseApp or FirebaseOptions. + * @param obj - an object of type FirebaseApp, FirebaseOptions or FirebaseAppSettings. * * @returns true if the provide object is of type FirebaseApp. * * @internal */ export function _isFirebaseApp( - obj: FirebaseApp | FirebaseOptions + obj: FirebaseApp | FirebaseOptions | FirebaseAppSettings ): obj is FirebaseApp { return (obj as FirebaseApp).options !== undefined; } +/** + * + * @param obj - an object of type FirebaseApp, FirebaseOptions or FirebaseAppSettings. + * + * @returns true if the provided object is of type FirebaseServerAppImpl. + * + * @internal + */ +export function _isFirebaseServerAppSettings( + obj: FirebaseApp | FirebaseOptions | FirebaseAppSettings +): obj is FirebaseServerAppSettings { + if (_isFirebaseApp(obj)) { + return false; + } + return ( + 'authIdToken' in obj || + 'appCheckToken' in obj || + 'releaseOnDeref' in obj || + 'automaticDataCollectionEnabled' in obj + ); +} + /** * * @param obj - an object of type FirebaseApp. diff --git a/packages/app/src/registerCoreComponents.ts b/packages/app/src/registerCoreComponents.ts index f0141dbf155..f84445714f3 100644 --- a/packages/app/src/registerCoreComponents.ts +++ b/packages/app/src/registerCoreComponents.ts @@ -40,7 +40,7 @@ export function registerCoreComponents(variant?: string): void { // Register `app` package. registerVersion(name, version, variant); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); // Register platform SDK identifier (no version). registerVersion('fire-js', ''); diff --git a/packages/auth-compat/CHANGELOG.md b/packages/auth-compat/CHANGELOG.md index 4dfdf4ce661..401045ee12a 100644 --- a/packages/auth-compat/CHANGELOG.md +++ b/packages/auth-compat/CHANGELOG.md @@ -1,5 +1,29 @@ # @firebase/auth-compat +## 0.6.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/auth@1.11.0 + - @firebase/util@1.13.0 + +## 0.5.28 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/auth@1.10.8 + - @firebase/component@0.6.18 + ## 0.5.27 ### Patch Changes diff --git a/packages/auth-compat/package.json b/packages/auth-compat/package.json index c27a8e1f31a..83f02a17be4 100644 --- a/packages/auth-compat/package.json +++ b/packages/auth-compat/package.json @@ -1,11 +1,11 @@ { "name": "@firebase/auth-compat", - "version": "0.5.27", + "version": "0.6.0", "description": "FirebaseAuth compatibility package that uses API style compatible with Firebase@8 and prior versions", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm2017.js", - "module": "dist/index.esm2017.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", "exports": { ".": { "types": "./dist/auth-compat/index.d.ts", @@ -16,9 +16,9 @@ }, "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/index.esm2017.js" + "import": "./dist/index.esm.js" }, - "default": "./dist/index.esm2017.js" + "default": "./dist/index.esm.js" }, "./package.json": "./package.json" }, @@ -49,15 +49,15 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/auth": "1.10.7", + "@firebase/auth": "1.11.0", "@firebase/auth-types": "0.13.0", - "@firebase/component": "0.6.17", - "@firebase/util": "1.12.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app-compat": "0.4.1", + "@firebase/app-compat": "0.5.3", "@rollup/plugin-json": "6.1.0", "rollup": "2.79.2", "rollup-plugin-replace": "2.2.0", @@ -81,6 +81,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index 5277c61cfa8..6439b542480 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,5 +1,28 @@ # @firebase/auth +## 1.11.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 1.10.8 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 1.10.7 ### Patch Changes diff --git a/packages/auth/api-extractor.json b/packages/auth/api-extractor.json index f7279fcac15..8b44b43f4d1 100644 --- a/packages/auth/api-extractor.json +++ b/packages/auth/api-extractor.json @@ -1,6 +1,6 @@ { "extends": "../../config/api-extractor.json", - "mainEntryPointFilePath": "/dist/esm2017/index.d.ts", + "mainEntryPointFilePath": "/dist/esm/index.d.ts", "dtsRollup": { "enabled": true, "untrimmedFilePath": "/dist/.d.ts", diff --git a/packages/auth/demo/rollup.config.js b/packages/auth/demo/rollup.config.js index ce6a2893210..d71d9aa58e3 100644 --- a/packages/auth/demo/rollup.config.js +++ b/packages/auth/demo/rollup.config.js @@ -31,11 +31,11 @@ const workerPlugins = [ tsconfigOverride: { compilerOptions: { declaration: false, - target: 'es2017', + target: 'es2020', lib: [ // TODO: remove this 'dom', - 'es2017', + 'es2020', 'webworker' ] } diff --git a/packages/auth/demo/src/worker/tsconfig.json b/packages/auth/demo/src/worker/tsconfig.json index 1081b12233f..3d05a4350c7 100644 --- a/packages/auth/demo/src/worker/tsconfig.json +++ b/packages/auth/demo/src/worker/tsconfig.json @@ -2,9 +2,9 @@ "extends": "../../../config/tsconfig.base.json", "compilerOptions": { "outDir": "dist", - "target": "es2017", + "target": "es2020", "lib": [ - "es2017", + "es2020", "webworker" ] }, diff --git a/packages/auth/internal/package.json b/packages/auth/internal/package.json index 4ecfdccbd20..5ec4fb3721a 100644 --- a/packages/auth/internal/package.json +++ b/packages/auth/internal/package.json @@ -2,11 +2,11 @@ "name": "@firebase/auth/internal", "description": "An internal version of the Auth SDK for use in the compatibility layer", "main": "../dist/node/internal.js", - "module": "../dist/esm2017/internal.js", - "browser": "../dist/esm2017/internal.js", - "typings": "../dist/esm2017/internal/index.d.ts", + "module": "../dist/esm/internal.js", + "browser": "../dist/esm/internal.js", + "typings": "../dist/esm/internal/index.d.ts", "private": true, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/auth/package.json b/packages/auth/package.json index 9ec35cfaec2..9a49314e2c1 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,14 +1,14 @@ { "name": "@firebase/auth", - "version": "1.10.7", + "version": "1.11.0", "description": "The Firebase Authenticaton component of the Firebase JS SDK.", "author": "Firebase (https://firebase.google.com/)", "main": "dist/node/index.js", "react-native": "dist/rn/index.js", - "browser": "dist/esm2017/index.js", - "module": "dist/esm2017/index.js", + "browser": "dist/esm/index.js", + "module": "dist/esm/index.js", "cordova": "dist/cordova/index.js", - "web-extension": "dist/web-extension-esm2017/index.js", + "web-extension": "dist/web-extension-esm/index.js", "webworker": "dist/index.webworker.js", "exports": { ".": { @@ -32,19 +32,19 @@ }, "browser": { "require": "./dist/browser-cjs/index.js", - "import": "./dist/esm2017/index.js" + "import": "./dist/esm/index.js" }, - "default": "./dist/esm2017/index.js" + "default": "./dist/esm/index.js" }, "./cordova": { "types": "./dist/cordova/auth-cordova-public.d.ts", "default": "./dist/cordova/index.js" }, "./web-extension": { - "types:": "./dist/web-extension-esm2017/auth-web-extension-public.d.ts", - "import": "./dist/web-extension-esm2017/index.js", + "types:": "./dist/web-extension-esm/auth-web-extension-public.d.ts", + "import": "./dist/web-extension-esm/index.js", "require": "./dist/web-extension-cjs/index.js", - "default": "./dist/web-extension-esm2017/index.js" + "default": "./dist/web-extension-esm/index.js" }, "./internal": { "types": "./dist/internal/index.d.ts", @@ -63,15 +63,15 @@ }, "browser": { "require": "./dist/browser-cjs/internal.js", - "import": "./dist/esm2017/internal.js" + "import": "./dist/esm/internal.js" }, "web-extension": { "types:": "./dist/web-extension-cjs/internal/index.d.ts", - "import": "./dist/web-extension-esm2017/internal.js", + "import": "./dist/web-extension-esm/internal.js", "require": "./dist/web-extension-cjs/internal.js", - "default": "./dist/web-extension-esm2017/internal.js" + "default": "./dist/web-extension-esm/internal.js" }, - "default": "./dist/esm2017/internal.js" + "default": "./dist/esm/internal.js" }, "./package.json": "./package.json" }, @@ -124,14 +124,14 @@ } }, "dependencies": { - "@firebase/component": "0.6.17", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-strip": "2.1.0", "@types/express": "4.17.21", @@ -160,6 +160,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/auth/rollup.config.js b/packages/auth/rollup.config.js index 01ca456e0ac..bef5b696ca2 100644 --- a/packages/auth/rollup.config.js +++ b/packages/auth/rollup.config.js @@ -61,13 +61,13 @@ const browserBuilds = [ internal: 'internal/index.ts' }, output: { - dir: 'dist/esm2017', + dir: 'dist/esm', format: 'es', sourcemap: true }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) }, @@ -79,7 +79,7 @@ const browserBuilds = [ output: [{ dir: 'dist/browser-cjs', format: 'cjs', sourcemap: true }], plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } @@ -92,13 +92,13 @@ const browserWebExtensionBuilds = [ internal: 'internal/index.ts' }, output: { - dir: 'dist/web-extension-esm2017', + dir: 'dist/web-extension-esm', format: 'es', sourcemap: true }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) @@ -111,7 +111,7 @@ const browserWebExtensionBuilds = [ output: [{ dir: 'dist/web-extension-cjs', format: 'cjs', sourcemap: true }], plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } @@ -127,7 +127,7 @@ const nodeBuilds = [ plugins: [ nodeAliasPlugin, ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) }, @@ -140,7 +140,7 @@ const nodeBuilds = [ plugins: [ nodeAliasPlugin, ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) @@ -159,7 +159,7 @@ const cordovaBuild = { }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], external: id => [...deps, 'cordova'].some(dep => id === dep || id.startsWith(`${dep}/`)) @@ -173,7 +173,7 @@ const rnBuild = { output: [{ dir: 'dist/rn', format: 'cjs', sourcemap: true }], plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: id => [...deps, 'react-native'].some( @@ -200,12 +200,12 @@ const webWorkerBuild = { lib: [ // Remove dom after we figure out why navigator stuff doesn't exist 'dom', - 'es2017', + 'es2020', 'webworker' ] } }), - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) }; diff --git a/packages/auth/src/core/auth/register.ts b/packages/auth/src/core/auth/register.ts index 9d0d6b4559d..4d7688e9804 100644 --- a/packages/auth/src/core/auth/register.ts +++ b/packages/auth/src/core/auth/register.ts @@ -130,6 +130,6 @@ export function registerAuth(clientPlatform: ClientPlatform): void { ); registerVersion(name, version, getVersionForPlatform(clientPlatform)); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/auth/src/core/providers/facebook.test.ts b/packages/auth/src/core/providers/facebook.test.ts index 7f71d04cc94..30e42648404 100644 --- a/packages/auth/src/core/providers/facebook.test.ts +++ b/packages/auth/src/core/providers/facebook.test.ts @@ -35,6 +35,11 @@ describe('core/providers/facebook', () => { expect(cred.signInMethod).to.eq(SignInMethod.FACEBOOK); }); + it('generates Facebook provider', () => { + const provider = new FacebookAuthProvider(); + expect(provider.providerId).to.eq(ProviderId.FACEBOOK); + }); + it('credentialFromResult creates the cred from a tagged result', async () => { const auth = await testAuth(); const userCred = new UserCredentialImpl({ @@ -66,4 +71,65 @@ describe('core/providers/facebook', () => { expect(cred.providerId).to.eq(ProviderId.FACEBOOK); expect(cred.signInMethod).to.eq(SignInMethod.FACEBOOK); }); + + it('returns null when _tokenResponse is missing', () => { + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'foo' + }); + error.customData = {}; // no _tokenResponse + + const cred = FacebookAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + }); + + it('returns null when _tokenResponse is missing oauthAccessToken key', () => { + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'foo' + }); + error.customData = { + _tokenResponse: { + // intentionally missing oauthAccessToken + idToken: 'some-id-token', + oauthAccessToken: null + } + }; + + const cred = FacebookAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + }); + + it('returns null when FacebookAuthProvider.credential throws', () => { + // Temporarily stub credential method to throw + const original = FacebookAuthProvider.credential; + FacebookAuthProvider.credential = () => { + throw new Error('Simulated failure'); + }; + + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'foo' + }); + error.customData = { + _tokenResponse: { + oauthAccessToken: 'valid-token' + } + }; + + const cred = FacebookAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + + // Restore original method + FacebookAuthProvider.credential = original; + }); + + it('returns null when error.customData is undefined (falls back to empty object)', () => { + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'foo' + }); + + // Don't set `customData` at all → fallback to {} + delete (error as any).customData; + + const cred = FacebookAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + }); }); diff --git a/packages/auth/src/core/providers/github.test.ts b/packages/auth/src/core/providers/github.test.ts index 9e7d0d73de8..2cb15a1be0d 100644 --- a/packages/auth/src/core/providers/github.test.ts +++ b/packages/auth/src/core/providers/github.test.ts @@ -35,6 +35,11 @@ describe('core/providers/github', () => { expect(cred.signInMethod).to.eq(SignInMethod.GITHUB); }); + it('generates Github provider', () => { + const provider = new GithubAuthProvider(); + expect(provider.providerId).to.eq(ProviderId.GITHUB); + }); + it('credentialFromResult creates the cred from a tagged result', async () => { const auth = await testAuth(); const userCred = new UserCredentialImpl({ @@ -66,4 +71,65 @@ describe('core/providers/github', () => { expect(cred.providerId).to.eq(ProviderId.GITHUB); expect(cred.signInMethod).to.eq(SignInMethod.GITHUB); }); + + it('returns null when _tokenResponse is missing', () => { + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'foo' + }); + error.customData = {}; // no _tokenResponse + + const cred = GithubAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + }); + + it('returns null when _tokenResponse is missing oauthAccessToken key', () => { + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'foo' + }); + error.customData = { + _tokenResponse: { + // intentionally missing oauthAccessToken + idToken: 'some-id-token', + oauthAccessToken: null + } + }; + + const cred = GithubAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + }); + + it('returns null when GithubAuthProvider.credential throws', () => { + // Temporarily stub credential method to throw + const original = GithubAuthProvider.credential; + GithubAuthProvider.credential = () => { + throw new Error('Simulated failure'); + }; + + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'foo' + }); + error.customData = { + _tokenResponse: { + oauthAccessToken: 'valid-token' + } + }; + + const cred = GithubAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + + // Restore original method + GithubAuthProvider.credential = original; + }); + + it('returns null when error.customData is undefined (falls back to empty object)', () => { + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'foo' + }); + + // Don't set `customData` at all → fallback to {} + delete (error as any).customData; + + const cred = GithubAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + }); }); diff --git a/packages/auth/src/core/providers/saml.test.ts b/packages/auth/src/core/providers/saml.test.ts index b2e714c7918..f4a5f3a187a 100644 --- a/packages/auth/src/core/providers/saml.test.ts +++ b/packages/auth/src/core/providers/saml.test.ts @@ -22,6 +22,7 @@ import { OperationType } from '../../model/enums'; import { TEST_ID_TOKEN_RESPONSE } from '../../../test/helpers/id_token_response'; import { testUser, testAuth } from '../../../test/helpers/mock_auth'; import { TaggedWithTokenResponse } from '../../model/id_token'; +import { SAMLAuthCredential } from '../credentials/saml'; import { AuthErrorCode } from '../errors'; import { UserCredentialImpl } from '../user/user_credential_impl'; import { _createError } from '../util/assert'; @@ -45,6 +46,17 @@ describe('core/providers/saml', () => { expect(cred.signInMethod).to.eq('saml.provider'); }); + it('generates SAML provider', () => { + const provider = new SAMLAuthProvider('saml.provider'); + expect(provider.providerId).to.eq('saml.provider'); + }); + + it('returns error for invalid SAML provdier', () => { + expect(() => { + new SAMLAuthProvider('provider'); + }).throw(/auth\/argument-error/); + }); + it('credentialFromResult returns null if provider ID not specified', async () => { const auth = await testAuth(); const userCred = new UserCredentialImpl({ @@ -73,4 +85,78 @@ describe('core/providers/saml', () => { expect(cred.providerId).to.eq('saml.provider'); expect(cred.signInMethod).to.eq('saml.provider'); }); + + it('credentialFromJSON returns SAML credential from valid object', () => { + const json = { + providerId: 'saml.provider', + signInMethod: 'saml.provider', + pendingToken: 'fake-pending-token' + }; + + const credential = SAMLAuthProvider.credentialFromJSON(json); + expect(credential.providerId).to.eq('saml.provider'); + expect(credential.signInMethod).to.eq('saml.provider'); + expect((credential as any).pendingToken).to.eq('fake-pending-token'); + }); + + it('returns null when _tokenResponse is missing (undefined)', () => { + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'test-app' + }); + + error.customData = {}; // _tokenResponse missing + const credential = SAMLAuthProvider.credentialFromError(error); + expect(credential).to.be.null; + }); + + it('returns null when _tokenResponse is missing oauthAccessToken key', () => { + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'foo' + }); + error.customData = { + _tokenResponse: { + // intentionally missing oauthAccessToken + idToken: 'some-id-token', + oauthAccessToken: null + } + }; + + const cred = SAMLAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + }); + + it('returns null if _create throws internally', () => { + const originalCreate = (SAMLAuthCredential as any)._create; + + (SAMLAuthCredential as any)._create = () => { + throw new Error('Simulated error'); + }; + + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'test-app' + }); + + error.customData = { + _tokenResponse: { + pendingToken: 'valid-token', + providerId: 'saml.my-provider' + } + }; + + const cred = SAMLAuthProvider.credentialFromError(error); + expect(cred).to.be.null; + + (SAMLAuthCredential as any)._create = originalCreate; + }); + + it('returns null when customData is undefined (falls back to empty object)', () => { + const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { + appName: 'test-app' + }); + + delete (error as any).customData; + + const credential = SAMLAuthProvider.credentialFromError(error); + expect(credential).to.be.null; + }); }); diff --git a/packages/auth/web-extension/api-extractor.json b/packages/auth/web-extension/api-extractor.json index d12063c69b5..111018d32ad 100644 --- a/packages/auth/web-extension/api-extractor.json +++ b/packages/auth/web-extension/api-extractor.json @@ -1,13 +1,13 @@ { "extends": "../../../config/api-extractor.json", - "mainEntryPointFilePath": "/dist/web-extension-esm2017/index.web-extension.d.ts", + "mainEntryPointFilePath": "/dist/web-extension-esm/index.web-extension.d.ts", "apiReport": { "enabled": false }, "dtsRollup": { "enabled": true, - "untrimmedFilePath": "/dist/web-extension-esm2017/.d.ts", - "publicTrimmedFilePath": "/dist/web-extension-esm2017/-public.d.ts" + "untrimmedFilePath": "/dist/web-extension-esm/.d.ts", + "publicTrimmedFilePath": "/dist/web-extension-esm/-public.d.ts" }, "docModel": { "enabled": true, diff --git a/packages/auth/web-extension/package.json b/packages/auth/web-extension/package.json index f3882a4f1d0..2b1b9ec599a 100644 --- a/packages/auth/web-extension/package.json +++ b/packages/auth/web-extension/package.json @@ -2,7 +2,7 @@ "name": "@firebase/auth-web-extension", "description": "A Chrome-Manifest-v3-specific build of the Firebase Auth JS SDK", "main": "../dist/web-extension-cjs/index.js", - "browser": "../dist/web-extension-esm2017/index.js", - "module": "../dist/web-extension-esm2017/index.js", - "typings": "../dist/web-extension-esm2017/auth-web-extension-public.d.ts" + "browser": "../dist/web-extension-esm/index.js", + "module": "../dist/web-extension-esm/index.js", + "typings": "../dist/web-extension-esm/auth-web-extension-public.d.ts" } \ No newline at end of file diff --git a/packages/component/CHANGELOG.md b/packages/component/CHANGELOG.md index f4e008fc2b1..b300eb047d9 100644 --- a/packages/component/CHANGELOG.md +++ b/packages/component/CHANGELOG.md @@ -1,5 +1,25 @@ # @firebase/component +## 0.7.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/util@1.13.0 + +## 0.6.18 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + ## 0.6.17 ### Patch Changes diff --git a/packages/component/package.json b/packages/component/package.json index b95204050f3..2b475f12177 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/component", - "version": "0.6.17", + "version": "0.7.0", "description": "Firebase Component Platform", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -31,7 +31,7 @@ "trusted-type-check": "tsec -p tsconfig.json --noEmit" }, "dependencies": { - "@firebase/util": "1.12.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "license": "Apache-2.0", @@ -56,6 +56,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/data-connect/CHANGELOG.md b/packages/data-connect/CHANGELOG.md index cb5e53a5c54..c62138b4d24 100644 --- a/packages/data-connect/CHANGELOG.md +++ b/packages/data-connect/CHANGELOG.md @@ -1,5 +1,24 @@ ## Unreleased +## 0.3.11 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.3.10 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 0.3.9 ### Patch Changes diff --git a/packages/data-connect/package.json b/packages/data-connect/package.json index 99b97e39f3a..29459b94f4e 100644 --- a/packages/data-connect/package.json +++ b/packages/data-connect/package.json @@ -1,11 +1,11 @@ { "name": "@firebase/data-connect", - "version": "0.3.9", + "version": "0.3.11", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm2017.js", - "module": "dist/index.esm2017.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", "exports": { ".": { "types": "./dist/public.d.ts", @@ -15,9 +15,9 @@ }, "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/index.esm2017.js" + "import": "./dist/index.esm.js" }, - "default": "./dist/index.esm2017.js" + "default": "./dist/index.esm.js" }, "./package.json": "./package.json" }, @@ -49,13 +49,13 @@ }, "dependencies": { "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.6.17", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" diff --git a/packages/data-connect/rollup.config.js b/packages/data-connect/rollup.config.js index ab0119ca5d2..7b004c0a421 100644 --- a/packages/data-connect/rollup.config.js +++ b/packages/data-connect/rollup.config.js @@ -59,7 +59,7 @@ const browserBuilds = [ ], plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], treeshake: { moduleSideEffects: false @@ -76,7 +76,7 @@ const browserBuilds = [ }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], treeshake: { moduleSideEffects: false @@ -96,7 +96,7 @@ const nodeBuilds = [ }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], treeshake: { moduleSideEffects: false @@ -113,7 +113,7 @@ const nodeBuilds = [ }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ], treeshake: { diff --git a/packages/data-connect/src/register.ts b/packages/data-connect/src/register.ts index 53b44f4e43d..badebf2a29b 100644 --- a/packages/data-connect/src/register.ts +++ b/packages/data-connect/src/register.ts @@ -58,6 +58,6 @@ export function registerDataConnect(variant?: string): void { ).setMultipleInstances(true) ); registerVersion(name, version, variant); - // BUILD_TARGET will be replaced by values like esm5, esm2017, cjs5, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/database-compat/CHANGELOG.md b/packages/database-compat/CHANGELOG.md index b15d780739a..9a4ec4fef66 100644 --- a/packages/database-compat/CHANGELOG.md +++ b/packages/database-compat/CHANGELOG.md @@ -1,5 +1,32 @@ # @firebase/database-compat +## 2.1.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/database@1.1.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + - @firebase/database-types@1.0.16 + +## 2.0.11 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/database@1.0.20 + - @firebase/database-types@1.0.15 + ## 2.0.10 ### Patch Changes diff --git a/packages/database-compat/package.json b/packages/database-compat/package.json index c746b8dde07..3c4460524be 100644 --- a/packages/database-compat/package.json +++ b/packages/database-compat/package.json @@ -1,11 +1,11 @@ { "name": "@firebase/database-compat", - "version": "2.0.10", + "version": "2.1.0", "description": "The Realtime Database component of the Firebase JS SDK.", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.js", - "browser": "dist/index.esm2017.js", - "module": "dist/index.esm2017.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", "license": "Apache-2.0", "typings": "dist/database-compat/src/index.d.ts", "files": [ @@ -22,9 +22,9 @@ }, "browser": { "require": "./dist/index.js", - "import": "./dist/index.esm2017.js" + "import": "./dist/index.esm.js" }, - "default": "./dist/index.esm2017.js" + "default": "./dist/index.esm.js" }, "./standalone": { "types": "./dist/database-compat/src/index.standalone.d.ts", @@ -49,15 +49,15 @@ "add-compat-overloads": "ts-node-script ../../scripts/build/create-overloads.ts -i ../database/dist/public.d.ts -o dist/database-compat/src/index.d.ts -a -r Database:types.FirebaseDatabase -r Query:types.Query -r DatabaseReference:types.Reference -r FirebaseApp:FirebaseAppCompat --moduleToEnhance @firebase/database" }, "dependencies": { - "@firebase/database": "1.0.19", - "@firebase/database-types": "1.0.14", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.4.1", + "@firebase/app-compat": "0.5.3", "typescript": "5.5.4" }, "repository": { @@ -69,6 +69,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/database-types/CHANGELOG.md b/packages/database-types/CHANGELOG.md index 316d77556a7..d175477dcd6 100644 --- a/packages/database-types/CHANGELOG.md +++ b/packages/database-types/CHANGELOG.md @@ -1,5 +1,19 @@ # @firebase/database-types +## 1.0.16 + +### Patch Changes + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/util@1.13.0 + +## 1.0.15 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + ## 1.0.14 ### Patch Changes diff --git a/packages/database-types/package.json b/packages/database-types/package.json index 95866297115..b63337a0612 100644 --- a/packages/database-types/package.json +++ b/packages/database-types/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/database-types", - "version": "1.0.14", + "version": "1.0.16", "description": "@firebase/database Types", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -13,7 +13,7 @@ ], "dependencies": { "@firebase/app-types": "0.9.3", - "@firebase/util": "1.12.0" + "@firebase/util": "1.13.0" }, "repository": { "directory": "packages/database-types", diff --git a/packages/database/CHANGELOG.md b/packages/database/CHANGELOG.md index 2c8060f7860..860266ce7a0 100644 --- a/packages/database/CHANGELOG.md +++ b/packages/database/CHANGELOG.md @@ -1,5 +1,28 @@ # Unreleased +## 1.1.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 1.0.20 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 1.0.19 ### Patch Changes diff --git a/packages/database/package.json b/packages/database/package.json index 54a549b9f68..305c42aac23 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -1,11 +1,11 @@ { "name": "@firebase/database", - "version": "1.0.19", + "version": "1.1.0", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm2017.js", - "module": "dist/index.esm2017.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", "standalone": "dist/index.standalone.js", "exports": { ".": { @@ -17,9 +17,9 @@ "standalone": "./dist/index.standalone.js", "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/index.esm2017.js" + "import": "./dist/index.esm.js" }, - "default": "./dist/index.esm2017.js" + "default": "./dist/index.esm.js" }, "./package.json": "./package.json" }, @@ -48,16 +48,16 @@ "license": "Apache-2.0", "peerDependencies": {}, "dependencies": { - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", "faye-websocket": "0.11.4", "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" @@ -78,6 +78,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/database/rollup.config.js b/packages/database/rollup.config.js index bdd6b8ae36c..165195229c9 100644 --- a/packages/database/rollup.config.js +++ b/packages/database/rollup.config.js @@ -59,7 +59,7 @@ const browserBuilds = [ ], plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], treeshake: { moduleSideEffects: false @@ -78,7 +78,7 @@ const browserBuilds = [ ], plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], treeshake: { moduleSideEffects: false @@ -98,7 +98,7 @@ const nodeBuilds = [ }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], treeshake: { moduleSideEffects: false @@ -115,7 +115,7 @@ const nodeBuilds = [ }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ], treeshake: { diff --git a/packages/database/src/register.ts b/packages/database/src/register.ts index 9322368526b..c54f52b5a3e 100644 --- a/packages/database/src/register.ts +++ b/packages/database/src/register.ts @@ -47,6 +47,6 @@ export function registerDatabase(variant?: string): void { ).setMultipleInstances(true) ); registerVersion(name, version, variant); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/firebase/CHANGELOG.md b/packages/firebase/CHANGELOG.md index 7885817632d..c21448da4f8 100644 --- a/packages/firebase/CHANGELOG.md +++ b/packages/firebase/CHANGELOG.md @@ -1,5 +1,165 @@ # firebase +## 12.3.0 + +### Minor Changes + +- [`06ab5c4`](https://github.com/firebase/firebase-js-sdk/commit/06ab5c4f9b84085068381f6dff5e03b1b7cf4b2c) [#9236](https://github.com/firebase/firebase-js-sdk/pull/9236) - Added a new `InferenceMode` option for the hybrid on-device capability: `prefer_in_cloud`. When this mode is selected, the SDK will attempt to use a cloud-hosted model first. If the call to the cloud-hosted model fails with a network-related error, the SDK will fall back to the on-device model, if it's available. + +- [`120a308`](https://github.com/firebase/firebase-js-sdk/commit/120a30838da50f5ade4f634e97c34cbfcaff41ba) [#9221](https://github.com/firebase/firebase-js-sdk/pull/9221) - Added support for Realtime Remote Config for the web. This feature introduces a new `onConfigUpdate` API and allows web applications to receive near-instant configuration updates without requiring periodic polling. + +- [`9b8ab02`](https://github.com/firebase/firebase-js-sdk/commit/9b8ab02c543785226fafec056d39be7cf7ee03d1) [#9249](https://github.com/firebase/firebase-js-sdk/pull/9249) - Added Code Execution feature. + +### Patch Changes + +- Updated dependencies [[`06ab5c4`](https://github.com/firebase/firebase-js-sdk/commit/06ab5c4f9b84085068381f6dff5e03b1b7cf4b2c), [`a4848b4`](https://github.com/firebase/firebase-js-sdk/commit/a4848b401f6e8da16b0d0fdbfd064e8d68566555), [`120a308`](https://github.com/firebase/firebase-js-sdk/commit/120a30838da50f5ade4f634e97c34cbfcaff41ba), [`9b8ab02`](https://github.com/firebase/firebase-js-sdk/commit/9b8ab02c543785226fafec056d39be7cf7ee03d1), [`c123766`](https://github.com/firebase/firebase-js-sdk/commit/c1237662e6851936d2dd6017ab4bc7f0aa5112fd), [`43276b0`](https://github.com/firebase/firebase-js-sdk/commit/43276b0414ea5a73e8d8f7e3b80275d8b910102f)]: + - @firebase/app@0.14.3 + - @firebase/ai@2.3.0 + - @firebase/remote-config@0.7.0 + - @firebase/firestore@4.9.2 + - @firebase/app-compat@0.5.3 + - @firebase/remote-config-compat@0.2.20 + - @firebase/firestore-compat@0.4.2 + +## 12.2.1 + +### Patch Changes + +- Updated dependencies [[`095c098`](https://github.com/firebase/firebase-js-sdk/commit/095c098de1e4399f3fb2993edae45060b2a8c6d0)]: + - @firebase/ai@2.2.1 + +## 12.2.0 + +### Minor Changes + +- [`984086b`](https://github.com/firebase/firebase-js-sdk/commit/984086b0b1bd607d3aac4cbb8400bc61416e2959) [#9224](https://github.com/firebase/firebase-js-sdk/pull/9224) - Add support for the Gemini Live API. + +- [`9b63cd6`](https://github.com/firebase/firebase-js-sdk/commit/9b63cd60efcd02b64b0d37f81affb3eabf70f9eb) [#9192](https://github.com/firebase/firebase-js-sdk/pull/9192) - Add `thoughtSummary()` convenience method to `EnhancedGenerateContentResponse`. + +- [`02280d7`](https://github.com/firebase/firebase-js-sdk/commit/02280d747863445fa1c21dfda01030412a6cecff) [#9201](https://github.com/firebase/firebase-js-sdk/pull/9201) - Add App Check limited use token option to `getAI()`. + +### Patch Changes + +- Updated dependencies [[`984086b`](https://github.com/firebase/firebase-js-sdk/commit/984086b0b1bd607d3aac4cbb8400bc61416e2959), [`9b63cd6`](https://github.com/firebase/firebase-js-sdk/commit/9b63cd60efcd02b64b0d37f81affb3eabf70f9eb), [`84b8bed`](https://github.com/firebase/firebase-js-sdk/commit/84b8bed35b69e4713fe8f677803cb06625525a61), [`c5f08a9`](https://github.com/firebase/firebase-js-sdk/commit/c5f08a9bc5da0d2b0207802c972d53724ccef055), [`02280d7`](https://github.com/firebase/firebase-js-sdk/commit/02280d747863445fa1c21dfda01030412a6cecff), [`2058432`](https://github.com/firebase/firebase-js-sdk/commit/2058432e6c8e809d5b695e31fde582e94f1349c5), [`5501791`](https://github.com/firebase/firebase-js-sdk/commit/5501791d0bd665c1c7d4fcd786053a46ceff208c), [`cbef6c6`](https://github.com/firebase/firebase-js-sdk/commit/cbef6c6e5b752c316104f9c834e0fe21b75c3ef1)]: + - @firebase/ai@2.2.0 + - @firebase/app@0.14.2 + - @firebase/firestore@4.9.1 + - @firebase/functions@0.13.1 + - @firebase/app-compat@0.5.2 + - @firebase/firestore-compat@0.4.1 + - @firebase/functions-compat@0.4.1 + +## 12.1.0 + +### Minor Changes + +- [`e25317f`](https://github.com/firebase/firebase-js-sdk/commit/e25317f9f3c58305bc093e4f2e676690feb16db0) [#9029](https://github.com/firebase/firebase-js-sdk/pull/9029) - Add hybrid inference options to the Firebase AI SDK. + +### Patch Changes + +- Updated dependencies [[`e25317f`](https://github.com/firebase/firebase-js-sdk/commit/e25317f9f3c58305bc093e4f2e676690feb16db0), [`a4897a6`](https://github.com/firebase/firebase-js-sdk/commit/a4897a621e99f270ddf6821d587fcddd3a0c5cd1)]: + - @firebase/app@0.14.1 + - @firebase/ai@2.1.0 + - @firebase/performance@0.7.9 + - @firebase/app-compat@0.5.1 + - @firebase/performance-compat@0.2.22 + +## 12.0.0 + +### Major Changes + +- [`5200f7b`](https://github.com/firebase/firebase-js-sdk/commit/5200f7bb777cf2260dcd396fbd19ac6cc7cb44c4) [#9042](https://github.com/firebase/firebase-js-sdk/pull/9042) - Add support for `anyOf` schemas + +- [`91fa484`](https://github.com/firebase/firebase-js-sdk/commit/91fa484b5a6081ad9c59d3b62416a2b5252b95a6) [#9081](https://github.com/firebase/firebase-js-sdk/pull/9081) - Remove `vertexai` import path + +- [`e59cd7d`](https://github.com/firebase/firebase-js-sdk/commit/e59cd7da1f375ec89f237ceb684c9f450d65cd34) [#9137](https://github.com/firebase/firebase-js-sdk/pull/9137) - Convert TS enums exports in Firebase AI into const variables. + +- [`cb19688`](https://github.com/firebase/firebase-js-sdk/commit/cb19688bf3d339a46c4964cb30b6263af08526e6) [#9079](https://github.com/firebase/firebase-js-sdk/pull/9079) - Remove GroundingAttribution + +- [`ec5f374`](https://github.com/firebase/firebase-js-sdk/commit/ec5f37403d9ebe28d3d71a7789d59edfb12762df) [#9063](https://github.com/firebase/firebase-js-sdk/pull/9063) - Remove `VertexAI` APIs. + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Minor Changes + +- [`a4ccd25`](https://github.com/firebase/firebase-js-sdk/commit/a4ccd254dd1ecb63aa010ca010ad50d4b8a8316a) [#9068](https://github.com/firebase/firebase-js-sdk/pull/9068) - Add support for Grounding with Google Search. + +- [`6ab4e13`](https://github.com/firebase/firebase-js-sdk/commit/6ab4e13a1665dab4be89ecc141b4584a5a6df569) [#9156](https://github.com/firebase/firebase-js-sdk/pull/9156) - Add support for Thinking Budget. + +- [`d91169f`](https://github.com/firebase/firebase-js-sdk/commit/d91169f061bf1dcbfe78a8c8a7f739677608fcb7) [#9151](https://github.com/firebase/firebase-js-sdk/pull/9151) (fixes [#8863](https://github.com/firebase/firebase-js-sdk/issues/8863)) - initializeServerApp now supports auto-initialization for Firebase App Hosting. + +### Patch Changes + +- Updated dependencies [[`a4ccd25`](https://github.com/firebase/firebase-js-sdk/commit/a4ccd254dd1ecb63aa010ca010ad50d4b8a8316a), [`5200f7b`](https://github.com/firebase/firebase-js-sdk/commit/5200f7bb777cf2260dcd396fbd19ac6cc7cb44c4), [`f11b552`](https://github.com/firebase/firebase-js-sdk/commit/f11b55294a04dfe6a1216c487b1af3a7e7d07196), [`6ab4e13`](https://github.com/firebase/firebase-js-sdk/commit/6ab4e13a1665dab4be89ecc141b4584a5a6df569), [`9771bff`](https://github.com/firebase/firebase-js-sdk/commit/9771bffadbc464890150dd7dd1a9a0fe2df60bf0), [`3d44792`](https://github.com/firebase/firebase-js-sdk/commit/3d44792f14f3df265162d06e2acdf3cad0c2ef86), [`ae976d0`](https://github.com/firebase/firebase-js-sdk/commit/ae976d02908a5a8913c5fcd4c0485fcf4b081fec), [`e59cd7d`](https://github.com/firebase/firebase-js-sdk/commit/e59cd7da1f375ec89f237ceb684c9f450d65cd34), [`cb19688`](https://github.com/firebase/firebase-js-sdk/commit/cb19688bf3d339a46c4964cb30b6263af08526e6), [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`d91169f`](https://github.com/firebase/firebase-js-sdk/commit/d91169f061bf1dcbfe78a8c8a7f739677608fcb7), [`ec5f374`](https://github.com/firebase/firebase-js-sdk/commit/ec5f37403d9ebe28d3d71a7789d59edfb12762df), [`a029ce3`](https://github.com/firebase/firebase-js-sdk/commit/a029ce39ee1ea1f6f28e79a1733ad8e8ebedf4bb), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/ai@2.0.0 + - @firebase/firestore@4.9.0 + - @firebase/performance@0.7.8 + - @firebase/installations-compat@0.2.19 + - @firebase/remote-config-compat@0.2.19 + - @firebase/performance-compat@0.2.21 + - @firebase/analytics-compat@0.2.24 + - @firebase/app-check-compat@0.4.0 + - @firebase/firestore-compat@0.4.0 + - @firebase/functions-compat@0.4.0 + - @firebase/messaging-compat@0.2.23 + - @firebase/database-compat@2.1.0 + - @firebase/storage-compat@0.4.0 + - @firebase/installations@0.6.19 + - @firebase/remote-config@0.6.6 + - @firebase/data-connect@0.3.11 + - @firebase/auth-compat@0.6.0 + - @firebase/app-compat@0.5.0 + - @firebase/analytics@0.10.18 + - @firebase/app-check@0.11.0 + - @firebase/functions@0.13.0 + - @firebase/messaging@0.12.23 + - @firebase/database@1.1.0 + - @firebase/storage@0.14.0 + - @firebase/auth@1.11.0 + - @firebase/util@1.13.0 + - @firebase/app@0.14.0 + +## 11.10.0 + +### Minor Changes + +- [`86155b3`](https://github.com/firebase/firebase-js-sdk/commit/86155b3c8f3974f8d777232625108c14f924e035) [#9115](https://github.com/firebase/firebase-js-sdk/pull/9115) - Added support for Firestore result types to be serialized with `toJSON` and then deserialized with `fromJSON` methods on the objects. + + Addeed support to resume `onSnapshot` listeners in the CSR phase based on serialized `DataSnapshot`s and `QuerySnapshot`s built in the SSR phase. + +### Patch Changes + +- [`13e6cce`](https://github.com/firebase/firebase-js-sdk/commit/13e6cce882d687e06c8d9bfb56895f8a77fc57b5) [#9085](https://github.com/firebase/firebase-js-sdk/pull/9085) - Add rollup config to generate modular typings for google3 + +- Updated dependencies [[`13e6cce`](https://github.com/firebase/firebase-js-sdk/commit/13e6cce882d687e06c8d9bfb56895f8a77fc57b5), [`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83), [`bb57947`](https://github.com/firebase/firebase-js-sdk/commit/bb57947c942e44b39e5b0254324bee6bf665fd4e), [`f73e08b`](https://github.com/firebase/firebase-js-sdk/commit/f73e08b212314547b39a10cd3e393f9e94776f21), [`86155b3`](https://github.com/firebase/firebase-js-sdk/commit/86155b3c8f3974f8d777232625108c14f924e035), [`b97eab3`](https://github.com/firebase/firebase-js-sdk/commit/b97eab36a3553c906c35f4751a0b17c717178b13)]: + - @firebase/remote-config@0.6.5 + - @firebase/analytics@0.10.17 + - @firebase/storage@0.13.14 + - @firebase/util@1.12.1 + - @firebase/app@0.13.2 + - @firebase/firestore@4.8.0 + - @firebase/ai@1.4.1 + - @firebase/remote-config-compat@0.2.18 + - @firebase/analytics-compat@0.2.23 + - @firebase/storage-compat@0.3.24 + - @firebase/app-check@0.10.1 + - @firebase/app-check-compat@0.3.26 + - @firebase/app-compat@0.4.2 + - @firebase/auth@1.10.8 + - @firebase/auth-compat@0.5.28 + - @firebase/data-connect@0.3.10 + - @firebase/database@1.0.20 + - @firebase/database-compat@2.0.11 + - @firebase/firestore-compat@0.3.53 + - @firebase/functions@0.12.9 + - @firebase/functions-compat@0.3.26 + - @firebase/installations@0.6.18 + - @firebase/installations-compat@0.2.18 + - @firebase/messaging@0.12.22 + - @firebase/messaging-compat@0.2.22 + - @firebase/performance@0.7.7 + - @firebase/performance-compat@0.2.20 + ## 11.9.1 ### Patch Changes diff --git a/packages/firebase/package.json b/packages/firebase/package.json index 4f785d15c8c..7d4ab8682ac 100644 --- a/packages/firebase/package.json +++ b/packages/firebase/package.json @@ -1,6 +1,6 @@ { "name": "firebase", - "version": "11.9.1", + "version": "12.3.0", "description": "Firebase JavaScript library for web and Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -239,18 +239,6 @@ }, "default": "./ai/dist/esm/index.esm.js" }, - "./vertexai": { - "types": "./ai/dist/ai/index.d.ts", - "node": { - "require": "./ai/dist/index.cjs.js", - "import": "./ai/dist/index.mjs" - }, - "browser": { - "require": "./ai/dist/index.cjs.js", - "import": "./ai/dist/esm/index.esm.js" - }, - "default": "./ai/dist/esm/index.esm.js" - }, "./compat/analytics": { "types": "./compat/analytics/dist/compat/analytics/index.d.ts", "node": { @@ -411,34 +399,34 @@ "trusted-type-check": "tsec -p tsconfig.json --noEmit" }, "dependencies": { - "@firebase/ai": "1.4.0", - "@firebase/app": "0.13.1", - "@firebase/app-compat": "0.4.1", + "@firebase/ai": "2.3.0", + "@firebase/app": "0.14.3", + "@firebase/app-compat": "0.5.3", "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.10.7", - "@firebase/auth-compat": "0.5.27", - "@firebase/data-connect": "0.3.9", - "@firebase/database": "1.0.19", - "@firebase/database-compat": "2.0.10", - "@firebase/firestore": "4.7.17", - "@firebase/firestore-compat": "0.3.52", - "@firebase/functions": "0.12.8", - "@firebase/functions-compat": "0.3.25", - "@firebase/installations": "0.6.17", - "@firebase/installations-compat": "0.2.17", - "@firebase/messaging": "0.12.21", - "@firebase/messaging-compat": "0.2.21", - "@firebase/storage": "0.13.13", - "@firebase/storage-compat": "0.3.23", - "@firebase/performance": "0.7.6", - "@firebase/performance-compat": "0.2.19", - "@firebase/remote-config": "0.6.4", - "@firebase/remote-config-compat": "0.2.17", - "@firebase/analytics": "0.10.16", - "@firebase/analytics-compat": "0.2.22", - "@firebase/app-check": "0.10.0", - "@firebase/app-check-compat": "0.3.25", - "@firebase/util": "1.12.0" + "@firebase/auth": "1.11.0", + "@firebase/auth-compat": "0.6.0", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-compat": "0.4.2", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20", + "@firebase/analytics": "0.10.18", + "@firebase/analytics-compat": "0.2.24", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/util": "1.13.0" }, "devDependencies": { "rollup": "2.79.2", @@ -472,7 +460,6 @@ "messaging", "messaging/sw", "database", - "vertexai", "data-connect" ], "typings": "empty.d.ts" diff --git a/packages/firebase/vertexai/package.json b/packages/firebase/vertexai/package.json deleted file mode 100644 index 3da541949dc..00000000000 --- a/packages/firebase/vertexai/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "firebase/vertexai", - "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm.js", - "module": "dist/esm/index.esm.js", - "typings": "dist/ai/index.d.ts" -} diff --git a/packages/firestore-compat/CHANGELOG.md b/packages/firestore-compat/CHANGELOG.md index 264da74339b..e5a1aa19d6e 100644 --- a/packages/firestore-compat/CHANGELOG.md +++ b/packages/firestore-compat/CHANGELOG.md @@ -1,5 +1,43 @@ # @firebase/firestore-compat +## 0.4.2 + +### Patch Changes + +- Updated dependencies [[`43276b0`](https://github.com/firebase/firebase-js-sdk/commit/43276b0414ea5a73e8d8f7e3b80275d8b910102f)]: + - @firebase/firestore@4.9.2 + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`2058432`](https://github.com/firebase/firebase-js-sdk/commit/2058432e6c8e809d5b695e31fde582e94f1349c5)]: + - @firebase/firestore@4.9.1 + +## 0.4.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f11b552`](https://github.com/firebase/firebase-js-sdk/commit/f11b55294a04dfe6a1216c487b1af3a7e7d07196), [`9771bff`](https://github.com/firebase/firebase-js-sdk/commit/9771bffadbc464890150dd7dd1a9a0fe2df60bf0), [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`a029ce3`](https://github.com/firebase/firebase-js-sdk/commit/a029ce39ee1ea1f6f28e79a1733ad8e8ebedf4bb), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/firestore@4.9.0 + - @firebase/component@0.7.0 + - @firebase/util@1.13.0 + +## 0.3.53 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83), [`f73e08b`](https://github.com/firebase/firebase-js-sdk/commit/f73e08b212314547b39a10cd3e393f9e94776f21), [`86155b3`](https://github.com/firebase/firebase-js-sdk/commit/86155b3c8f3974f8d777232625108c14f924e035)]: + - @firebase/util@1.12.1 + - @firebase/firestore@4.8.0 + - @firebase/component@0.6.18 + ## 0.3.52 ### Patch Changes diff --git a/packages/firestore-compat/package.json b/packages/firestore-compat/package.json index 4071cdb236f..0787c21f397 100644 --- a/packages/firestore-compat/package.json +++ b/packages/firestore-compat/package.json @@ -1,12 +1,12 @@ { "name": "@firebase/firestore-compat", - "version": "0.3.52", + "version": "0.4.2", "description": "The Cloud Firestore component of the Firebase JS SDK.", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", "react-native": "dist/index.rn.js", - "browser": "dist/index.esm2017.js", - "module": "dist/index.esm2017.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", @@ -17,9 +17,9 @@ "react-native": "./dist/index.rn.js", "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/index.esm2017.js" + "import": "./dist/index.esm.js" }, - "default": "./dist/index.esm2017.js" + "default": "./dist/index.esm.js" }, "./package.json": "./package.json" }, @@ -46,14 +46,14 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/component": "0.6.17", - "@firebase/firestore": "4.7.17", - "@firebase/util": "1.12.0", + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.2", + "@firebase/util": "1.13.0", "@firebase/firestore-types": "3.0.3", "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.4.1", + "@firebase/app-compat": "0.5.3", "@types/eslint": "7.29.0", "rollup": "2.79.2", "rollup-plugin-sourcemaps": "0.6.3", @@ -74,6 +74,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/firestore-compat/rollup.config.js b/packages/firestore-compat/rollup.config.js index c85af11225f..e265d3e31b7 100644 --- a/packages/firestore-compat/rollup.config.js +++ b/packages/firestore-compat/rollup.config.js @@ -25,12 +25,12 @@ const util = require('../firestore/rollup.shared'); const deps = Object.keys({ ...pkg.peerDependencies, ...pkg.dependencies }); -const es2017Plugins = [ +const es2020Plugins = [ typescriptPlugin({ typescript, tsconfigOverride: { compilerOptions: { - target: 'es2017' + target: 'es2020' } }, transformers: [util.removeAssertTransformer] @@ -46,7 +46,7 @@ const browserBuilds = [ format: 'es', sourcemap: true }, - plugins: es2017Plugins, + plugins: es2020Plugins, external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) }, { @@ -58,7 +58,7 @@ const browserBuilds = [ sourcemap: true } ], - plugins: es2017Plugins, + plugins: es2020Plugins, external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } ]; @@ -71,7 +71,7 @@ const nodeBuilds = [ format: 'cjs', sourcemap: true }, - plugins: es2017Plugins, + plugins: es2020Plugins, external: deps }, { @@ -81,7 +81,7 @@ const nodeBuilds = [ format: 'es', sourcemap: true }, - plugins: [...es2017Plugins, emitModulePackageFile()], + plugins: [...es2020Plugins, emitModulePackageFile()], external: deps } ]; @@ -94,7 +94,7 @@ const rnBuilds = [ format: 'es', sourcemap: true }, - plugins: es2017Plugins, + plugins: es2020Plugins, external: deps } ]; diff --git a/packages/firestore/CHANGELOG.md b/packages/firestore/CHANGELOG.md index 3c73ea511d9..b3d1f6d4626 100644 --- a/packages/firestore/CHANGELOG.md +++ b/packages/firestore/CHANGELOG.md @@ -1,5 +1,58 @@ # @firebase/firestore +## 4.9.2 + +### Patch Changes + +- [`43276b0`](https://github.com/firebase/firebase-js-sdk/commit/43276b0414ea5a73e8d8f7e3b80275d8b910102f) [#9242](https://github.com/firebase/firebase-js-sdk/pull/9242) - Increased the buffering-proxy detection timeout to minimize the false-positive rate. Updating WebChannel to ignore duplicate messages received from the server. Fix for https://github.com/firebase/firebase-js-sdk/issues/8250. + +- Updated dependencies [[`43276b0`](https://github.com/firebase/firebase-js-sdk/commit/43276b0414ea5a73e8d8f7e3b80275d8b910102f)]: + - @firebase/webchannel-wrapper@1.0.5 + +## 4.9.1 + +### Patch Changes + +- [`2058432`](https://github.com/firebase/firebase-js-sdk/commit/2058432e6c8e809d5b695e31fde582e94f1349c5) [#9177](https://github.com/firebase/firebase-js-sdk/pull/9177) (fixes [#9147](https://github.com/firebase/firebase-js-sdk/issues/9147)) - Fixed a bug where a rejected promise with an empty message in a transaction would cause a timeout. + +## 4.9.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f11b552`](https://github.com/firebase/firebase-js-sdk/commit/f11b55294a04dfe6a1216c487b1af3a7e7d07196) [#9162](https://github.com/firebase/firebase-js-sdk/pull/9162) - Revert fix for issue where Firestore would produce `undefined` for document snapshot if "clear site data" button was pressed in the web browser. This fix was introduced in v11.6.1 but inadvertantly caused issues for some customers (https://github.com/firebase/firebase-js-sdk/issues/9056). + +- [`9771bff`](https://github.com/firebase/firebase-js-sdk/commit/9771bffadbc464890150dd7dd1a9a0fe2df60bf0) [#9168](https://github.com/firebase/firebase-js-sdk/pull/9168) - Fixed a regression where the SDK did not re-connect to IndexedDb after disconnect (#9087) + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- [`a029ce3`](https://github.com/firebase/firebase-js-sdk/commit/a029ce39ee1ea1f6f28e79a1733ad8e8ebedf4bb) [#9143](https://github.com/firebase/firebase-js-sdk/pull/9143) - Further improved performance of UTF-8 string ordering logic, which had degraded in v11.3.0, was reverted in v11.3.1, and was re-introduced with some improvements in v11.5.0. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/webchannel-wrapper@1.0.4 + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 4.8.0 + +### Minor Changes + +- [`86155b3`](https://github.com/firebase/firebase-js-sdk/commit/86155b3c8f3974f8d777232625108c14f924e035) [#9115](https://github.com/firebase/firebase-js-sdk/pull/9115) - Added support for Firestore result types to be serialized with `toJSON` and then deserialized with `fromJSON` methods on the objects. + + Addeed support to resume `onSnapshot` listeners in the CSR phase based on serialized `DataSnapshot`s and `QuerySnapshot`s built in the SSR phase. + +### Patch Changes + +- [`f73e08b`](https://github.com/firebase/firebase-js-sdk/commit/f73e08b212314547b39a10cd3e393f9e94776f21) [#9087](https://github.com/firebase/firebase-js-sdk/pull/9087) - Internal listener registration change for IndexedDB "versionchange" events. + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 4.7.17 ### Patch Changes diff --git a/packages/firestore/lite/index.ts b/packages/firestore/lite/index.ts index 636eb4c6709..b751f0a8254 100644 --- a/packages/firestore/lite/index.ts +++ b/packages/firestore/lite/index.ts @@ -27,171 +27,6 @@ import { registerFirestore } from './register'; registerFirestore(); -export { PipelineSource } from '../src/lite-api/pipeline-source'; - -export { PipelineResult } from '../src/lite-api/pipeline-result'; - -export { Pipeline, pipeline } from '../src/lite-api/pipeline'; - -export { useFirestorePipelines } from '../src/lite-api/database_augmentation'; - -export { execute } from '../src/lite-api/pipeline_impl'; - -export { - Stage, - FindNearestOptions, - AddFields, - Aggregate, - Distinct, - CollectionSource, - CollectionGroupSource, - DatabaseSource, - DocumentsSource, - Where, - FindNearest, - Limit, - Offset, - Select, - Sort, - GenericStage -} from '../src/lite-api/stage'; - -export { - add, - subtract, - multiply, - divide, - mod, - eq, - neq, - lt, - lte, - gt, - gte, - arrayConcat, - arrayContains, - arrayContainsAny, - arrayContainsAll, - arrayLength, - inAny, - notInAny, - xor, - ifFunction, - not, - logicalMax, - logicalMin, - exists, - isNan, - reverse, - replaceFirst, - replaceAll, - byteLength, - charLength, - like, - regexContains, - regexMatch, - strContains, - startsWith, - endsWith, - toLower, - toUpper, - trim, - strConcat, - mapGet, - countAll, - min, - max, - cosineDistance, - dotProduct, - euclideanDistance, - vectorLength, - unixMicrosToTimestamp, - timestampToUnixMicros, - unixMillisToTimestamp, - timestampToUnixMillis, - unixSecondsToTimestamp, - timestampToUnixSeconds, - timestampAdd, - timestampSub, - genericFunction, - ascending, - descending, - ExprWithAlias, - Field, - Fields, - Constant, - FirestoreFunction, - Add, - Subtract, - Multiply, - Divide, - Mod, - Eq, - Neq, - Lt, - Lte, - Gt, - Gte, - ArrayConcat, - ArrayReverse, - ArrayContains, - ArrayContainsAll, - ArrayContainsAny, - ArrayLength, - ArrayElement, - In, - IsNan, - Exists, - Not, - And, - Or, - Xor, - If, - LogicalMax, - LogicalMin, - Reverse, - ReplaceFirst, - ReplaceAll, - CharLength, - ByteLength, - Like, - RegexContains, - RegexMatch, - StrContains, - StartsWith, - EndsWith, - ToLower, - ToUpper, - Trim, - StrConcat, - MapGet, - Count, - Sum, - Avg, - Min, - Max, - CosineDistance, - DotProduct, - EuclideanDistance, - VectorLength, - UnixMicrosToTimestamp, - TimestampToUnixMicros, - UnixMillisToTimestamp, - TimestampToUnixMillis, - UnixSecondsToTimestamp, - TimestampToUnixSeconds, - TimestampAdd, - TimestampSub, - Ordering, - ExprType, - AccumulatorTarget, - FilterExpr, - SelectableExpr, - Selectable, - FilterCondition, - Accumulator -} from '../src/lite-api/expressions'; - export { aggregateQuerySnapshotEqual, getCount, diff --git a/packages/firestore/lite/package.json b/packages/firestore/lite/package.json index ef362535fdb..5744f1d65d6 100644 --- a/packages/firestore/lite/package.json +++ b/packages/firestore/lite/package.json @@ -3,12 +3,12 @@ "description": "A lite version of the Firestore SDK", "main": "../dist/lite/index.node.cjs.js", "main-esm": "../dist/lite/index.node.mjs", - "module": "../dist/lite/index.browser.esm2017.js", - "browser": "../dist/lite/index.browser.esm2017.js", - "react-native": "../dist/lite/index.rn.esm2017.js", + "module": "../dist/lite/index.browser.esm.js", + "browser": "../dist/lite/index.browser.esm.js", + "react-native": "../dist/lite/index.rn.esm.js", "typings": "../dist/lite/index.d.ts", "private": true, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 638b8914483..4001c19fe8c 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -1,8 +1,8 @@ { "name": "@firebase/firestore", - "version": "4.7.17", + "version": "4.9.2", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "description": "The Cloud Firestore component of the Firebase JS SDK.", "author": "Firebase (https://firebase.google.com/)", @@ -70,9 +70,9 @@ "react-native": "./dist/index.rn.js", "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/index.esm2017.js" + "import": "./dist/index.esm.js" }, - "default": "./dist/index.esm2017.js" + "default": "./dist/index.esm.js" }, "./lite": { "types": "./dist/lite/index.d.ts", @@ -80,30 +80,30 @@ "require": "./dist/lite/index.node.cjs.js", "import": "./dist/lite/index.node.mjs" }, - "react-native": "./dist/lite/index.rn.esm2017.js", + "react-native": "./dist/lite/index.rn.esm.js", "browser": { "require": "./dist/lite/index.cjs.js", - "import": "./dist/lite/index.browser.esm2017.js" + "import": "./dist/lite/index.browser.esm.js" }, - "default": "./dist/lite/index.browser.esm2017.js" + "default": "./dist/lite/index.browser.esm.js" }, "./package.json": "./package.json" }, "main": "dist/index.node.cjs.js", "main-esm": "dist/index.node.mjs", "react-native": "dist/index.rn.js", - "browser": "dist/index.esm2017.js", - "module": "dist/index.esm2017.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", "license": "Apache-2.0", "files": [ "dist", "lite/package.json" ], "dependencies": { - "@firebase/component": "0.6.17", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", - "@firebase/webchannel-wrapper": "1.0.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.5", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", "tslib": "^2.1.0" @@ -112,9 +112,9 @@ "@firebase/app": "0.x" }, "devDependencies": { - "@firebase/app": "0.13.1", - "@firebase/app-compat": "0.4.1", - "@firebase/auth": "1.10.7", + "@firebase/app": "0.14.3", + "@firebase/app-compat": "0.5.3", + "@firebase/auth": "1.11.0", "@rollup/plugin-alias": "5.1.1", "@rollup/plugin-json": "6.1.0", "@types/eslint": "7.29.0", diff --git a/packages/firestore/rollup.config.debug.js b/packages/firestore/rollup.config.debug.js index a8823820d1d..10b2870e18b 100644 --- a/packages/firestore/rollup.config.debug.js +++ b/packages/firestore/rollup.config.debug.js @@ -25,9 +25,9 @@ import pkg from './package.json'; // This rollup configuration creates a single non-minified build for browser // testing. You can test code changes by running `yarn build:debug`. This -// creates the file "dist/index.esm2017.js" that you can use in your sample +// creates the file "dist/index.esm.js" that you can use in your sample // app as a replacement for -// "node_modules/@firebase/firestore/dist/index.esm2017.js". +// "node_modules/@firebase/firestore/dist/index.esm.js". const browserPlugins = function () { return [ @@ -35,7 +35,7 @@ const browserPlugins = function () { typescript, tsconfigOverride: { compilerOptions: { - target: 'es2017' + target: 'es2020' } }, cacheDir: tmp.dirSync(), diff --git a/packages/firestore/rollup.config.js b/packages/firestore/rollup.config.js index f9a29bef742..5fa616bca80 100644 --- a/packages/firestore/rollup.config.js +++ b/packages/firestore/rollup.config.js @@ -97,7 +97,7 @@ const allBuilds = [ cacheDir: tmp.dirSync() }), sourcemaps(), - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: util.resolveNodeExterns, treeshake: { @@ -114,7 +114,7 @@ const allBuilds = [ }, plugins: [ sourcemaps(), - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], external: util.resolveNodeExterns, treeshake: { @@ -137,7 +137,7 @@ const allBuilds = [ moduleSideEffects: false } }, - // Convert es2017 build to cjs + // Convert es2020 build to cjs { input: pkg['browser'], output: [ @@ -149,14 +149,14 @@ const allBuilds = [ ], plugins: [ sourcemaps(), - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: util.resolveBrowserExterns, treeshake: { moduleSideEffects: false } }, - // es2017 build with build target reporting + // es2020 build with build target reporting { input: pkg['browser'], output: [ @@ -168,7 +168,7 @@ const allBuilds = [ ], plugins: [ sourcemaps(), - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], external: util.resolveBrowserExterns, treeshake: { @@ -186,7 +186,7 @@ const allBuilds = [ plugins: [ alias(util.generateAliasConfig('rn')), ...browserPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], external: util.resolveBrowserExterns, treeshake: { diff --git a/packages/firestore/rollup.config.lite.js b/packages/firestore/rollup.config.lite.js index 9ff4d57a8d8..5ea2225f364 100644 --- a/packages/firestore/rollup.config.lite.js +++ b/packages/firestore/rollup.config.lite.js @@ -93,7 +93,7 @@ const allBuilds = [ }), json(), sourcemaps(), - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: util.resolveNodeExterns, treeshake: { @@ -110,7 +110,7 @@ const allBuilds = [ }, plugins: [ sourcemaps(), - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], external: util.resolveNodeExterns, treeshake: { @@ -140,7 +140,7 @@ const allBuilds = [ moduleSideEffects: false } }, - // Convert es2017 build to CJS + // Convert es2020 build to CJS { input: path.resolve('./lite', pkg.browser), output: [ @@ -152,14 +152,14 @@ const allBuilds = [ ], plugins: [ sourcemaps(), - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: util.resolveBrowserExterns, treeshake: { moduleSideEffects: false } }, - // Browser es2017 build + // Browser es2020 build { input: path.resolve('./lite', pkg.browser), output: [ @@ -171,7 +171,7 @@ const allBuilds = [ ], plugins: [ sourcemaps(), - replace(generateBuildTargetReplaceConfig('esm', 2017)) + replace(generateBuildTargetReplaceConfig('esm', 2020)) ], external: util.resolveBrowserExterns, treeshake: { @@ -190,7 +190,7 @@ const allBuilds = [ alias(util.generateAliasConfig('rn_lite')), ...browserPlugins, replace({ - ...generateBuildTargetReplaceConfig('esm', 2017), + ...generateBuildTargetReplaceConfig('esm', 2020), '__RUNTIME_ENV__': 'rn' }) ], diff --git a/packages/firestore/rollup.shared.js b/packages/firestore/rollup.shared.js index 728f03df2fe..99adca9096e 100644 --- a/packages/firestore/rollup.shared.js +++ b/packages/firestore/rollup.shared.js @@ -33,8 +33,8 @@ const pkg = require('./package.json'); // This file contains shared utilities for Firestore's rollup builds. // Firestore is released in a number of different build configurations: -// - Browser builds that support persistence in ES2017 CJS and ESM formats. -// - In-memory Browser builds that support persistence in ES2017 CJS and ESM +// - Browser builds that support persistence in ES2020 CJS and ESM formats. +// - In-memory Browser builds that support persistence in ES2020 CJS and ESM // formats. // - A NodeJS build that supports persistence (to be used with an IndexedDb // shim) @@ -46,7 +46,7 @@ const pkg = require('./package.json'); // We use two different rollup pipelines to take advantage of tree shaking, // as Rollup does not support tree shaking for TypeScript classes transpiled // down to ES5 (see https://bit.ly/340P23U). The build pipeline in this file -// produces tree-shaken ES2017 builds that are consumed by the ES5 builds in +// produces tree-shaken ES2020 builds that are consumed by the ES5 builds in // `rollup.config.es.js`. // // All browser builds rely on Terser's property name mangling to reduce code @@ -240,7 +240,7 @@ exports.applyPrebuilt = function (name = 'prebuilt.js') { }); }; -exports.es2017Plugins = function (platform, mangled = false) { +exports.es2020Plugins = function (platform, mangled = false) { if (mangled) { return [ alias(generateAliasConfig(platform)), @@ -265,7 +265,7 @@ exports.es2017Plugins = function (platform, mangled = false) { } }; -exports.es2017PluginsCompat = function ( +exports.es2020PluginsCompat = function ( platform, pathTransformer, mangled = false diff --git a/packages/firestore/scripts/build-bundle.js b/packages/firestore/scripts/build-bundle.js index f8ba283a5a8..89153e74540 100644 --- a/packages/firestore/scripts/build-bundle.js +++ b/packages/firestore/scripts/build-bundle.js @@ -14,4 +14,4 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){function adopt(value){return value instanceof P?value:new P((function(resolve){resolve(value)}))}return new(P||(P=Promise))((function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator["throw"](value))}catch(e){reject(e)}}function step(result){result.done?resolve(result.value):adopt(result.value).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())}))};var __generator=this&&this.__generator||function(thisArg,body){var _={label:0,sent:function(){if(t[0]&1)throw t[1];return t[1]},trys:[],ops:[]},f,y,t,g;return g={next:verb(0),throw:verb(1),return:verb(2)},typeof Symbol==="function"&&(g[Symbol.iterator]=function(){return this}),g;function verb(n){return function(v){return step([n,v])}}function step(op){if(f)throw new TypeError("Generator is already executing.");while(g&&(g=0,op[0]&&(_=0)),_)try{if(f=1,y&&(t=op[0]&2?y["return"]:op[0]?y["throw"]||((t=y["return"])&&t.call(y),0):y.next)&&!(t=t.call(y,op[1])).done)return t;if(y=0,t)op=[op[0]&2,t.value];switch(op[0]){case 0:case 1:t=op;break;case 4:_.label++;return{value:op[1],done:false};case 5:_.label++;y=op[1];op=[0];continue;case 7:op=_.ops.pop();_.trys.pop();continue;default:if(!(t=_.trys,t=t.length>0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1] { typescriptPlugin({ tsconfigOverride: { compilerOptions: { - target: 'es2017' + target: 'es2020' } }, transformers: [util.removeAssertTransformer] diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index 5aa85f55637..d05f032a910 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -15,176 +15,6 @@ * limitations under the License. */ -export { PipelineSource } from './lite-api/pipeline-source'; - -export { PipelineResult } from './lite-api/pipeline-result'; - -export { Pipeline, pipeline } from './api/pipeline'; - -export { useFirestorePipelines } from './api/database_augmentation'; - -export { execute } from './lite-api/pipeline_impl'; - -export { - Stage, - FindNearestOptions, - AddFields, - Aggregate, - Distinct, - CollectionSource, - CollectionGroupSource, - DatabaseSource, - DocumentsSource, - Where, - FindNearest, - Limit, - Offset, - Select, - Sort, - GenericStage -} from './lite-api/stage'; - -export { - add, - subtract, - multiply, - divide, - mod, - eq, - neq, - lt, - lte, - gt, - gte, - arrayConcat, - arrayContains, - arrayContainsAny, - arrayContainsAll, - arrayLength, - inAny, - notInAny, - xor, - ifFunction, - not, - logicalMax, - logicalMin, - exists, - isNan, - reverse, - replaceFirst, - replaceAll, - byteLength, - charLength, - like, - regexContains, - regexMatch, - strContains, - startsWith, - endsWith, - toLower, - toUpper, - trim, - strConcat, - mapGet, - countAll, - countFunction, - sumFunction, - avgFunction, - andFunction, - orFunction, - min, - max, - cosineDistance, - dotProduct, - euclideanDistance, - vectorLength, - unixMicrosToTimestamp, - timestampToUnixMicros, - unixMillisToTimestamp, - timestampToUnixMillis, - unixSecondsToTimestamp, - timestampToUnixSeconds, - timestampAdd, - timestampSub, - genericFunction, - ascending, - descending, - ExprWithAlias, - Field, - Fields, - Constant, - FirestoreFunction, - Add, - Subtract, - Multiply, - Divide, - Mod, - Eq, - Neq, - Lt, - Lte, - Gt, - Gte, - ArrayConcat, - ArrayReverse, - ArrayContains, - ArrayContainsAll, - ArrayContainsAny, - ArrayLength, - ArrayElement, - In, - IsNan, - Exists, - Not, - And, - Or, - Xor, - If, - LogicalMax, - LogicalMin, - Reverse, - ReplaceFirst, - ReplaceAll, - CharLength, - ByteLength, - Like, - RegexContains, - RegexMatch, - StrContains, - StartsWith, - EndsWith, - ToLower, - ToUpper, - Trim, - StrConcat, - MapGet, - Count, - Sum, - Avg, - Min, - Max, - CosineDistance, - DotProduct, - EuclideanDistance, - VectorLength, - UnixMicrosToTimestamp, - TimestampToUnixMicros, - UnixMillisToTimestamp, - TimestampToUnixMillis, - UnixSecondsToTimestamp, - TimestampToUnixSeconds, - TimestampAdd, - TimestampSub, - Ordering, - ExprType, - AccumulatorTarget, - FilterExpr, - SelectableExpr, - Selectable, - FilterCondition, - Accumulator -} from './lite-api/expressions'; - export { aggregateFieldEqual, aggregateQuerySnapshotEqual, @@ -397,8 +227,7 @@ export { isBase64Available as _isBase64Available } from './platform/base64'; export { DatabaseId as _DatabaseId } from './core/database_info'; export { _internalQueryToProtoQueryTarget, - _internalAggregationQueryToProtoRunAggregationQueryRequest, - _internalPipelineToExecutePipelineRequestProto + _internalAggregationQueryToProtoRunAggregationQueryRequest } from './remote/internal_serializer'; export { cast as _cast, diff --git a/packages/firestore/src/api/aggregate.ts b/packages/firestore/src/api/aggregate.ts index 453f9e0a841..f0e2c1e1dc0 100644 --- a/packages/firestore/src/api/aggregate.ts +++ b/packages/firestore/src/api/aggregate.ts @@ -15,21 +15,17 @@ * limitations under the License. */ +import { AggregateField, AggregateSpec, DocumentData, Query } from '../api'; import { AggregateImpl } from '../core/aggregate'; import { firestoreClientRunAggregateQuery } from '../core/firestore_client'; import { count } from '../lite-api/aggregate'; -import { - AggregateField, - AggregateQuerySnapshot, - AggregateSpec -} from '../lite-api/aggregate_types'; -import { DocumentData, Query } from '../lite-api/reference'; +import { AggregateQuerySnapshot } from '../lite-api/aggregate_types'; import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; import { cast } from '../util/input_validation'; import { mapToArray } from '../util/obj'; import { ensureFirestoreConfigured, Firestore } from './database'; -import { ExpUserDataWriter } from './user_data_writer'; +import { ExpUserDataWriter } from './reference_impl'; export { aggregateQuerySnapshotEqual, diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index feabdddae2d..a2feb19507f 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -51,7 +51,6 @@ import { connectFirestoreEmulator, Firestore as LiteFirestore } from '../lite-api/database'; -import type { PipelineSource } from '../lite-api/pipeline-source'; import { Query } from '../lite-api/reference'; import { indexedDbClearPersistence, @@ -134,15 +133,6 @@ export class Firestore extends LiteFirestore { await terminate; } } - - /** - * Pipeline query. - */ - pipeline(): PipelineSource { - throw new Error( - 'Pipelines not initialized. Your application must call `useFirestorePipelines()` before using Firestore Pipeline features.' - ); - } } /** diff --git a/packages/firestore/src/api/database_augmentation.ts b/packages/firestore/src/api/database_augmentation.ts deleted file mode 100644 index 0eb8c91a034..00000000000 --- a/packages/firestore/src/api/database_augmentation.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Pipeline } from '../lite-api/pipeline'; -import { PipelineSource } from '../lite-api/pipeline-source'; -import { newUserDataReader } from '../lite-api/user_data_reader'; -import { DocumentKey } from '../model/document_key'; - -import { Firestore } from './database'; -import { DocumentReference, Query } from './reference'; -import { ExpUserDataWriter } from './user_data_writer'; - -export function useFirestorePipelines(): void { - Firestore.prototype.pipeline = function (): PipelineSource { - const firestore = this; - return new PipelineSource( - this, - newUserDataReader(firestore), - new ExpUserDataWriter(firestore), - (key: DocumentKey) => { - return new DocumentReference(firestore, null, key); - } - ); - }; - - Query.prototype.pipeline = function (): Pipeline { - let pipeline; - if (this._query.collectionGroup) { - pipeline = this.firestore - .pipeline() - .collectionGroup(this._query.collectionGroup); - } else { - pipeline = this.firestore - .pipeline() - .collection(this._query.path.canonicalString()); - } - - // TODO(pipeline) convert existing query filters, limits, etc into - // pipeline stages - - return pipeline; - }; -} diff --git a/packages/firestore/src/api/pipeline.ts b/packages/firestore/src/api/pipeline.ts deleted file mode 100644 index 14532ba85c0..00000000000 --- a/packages/firestore/src/api/pipeline.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { firestoreClientExecutePipeline } from '../core/firestore_client'; -import { Pipeline as LitePipeline } from '../lite-api/pipeline'; -import { PipelineResult } from '../lite-api/pipeline-result'; -import { PipelineSource } from '../lite-api/pipeline-source'; -import { DocumentData, DocumentReference, Query } from '../lite-api/reference'; -import { Stage } from '../lite-api/stage'; -import { UserDataReader } from '../lite-api/user_data_reader'; -import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; -import { DocumentKey } from '../model/document_key'; -import { cast } from '../util/input_validation'; - -import { ensureFirestoreConfigured, Firestore } from './database'; - -export class Pipeline< - AppModelType = DocumentData -> extends LitePipeline { - /** - * @internal - * @private - * @param db - * @param userDataReader - * @param userDataWriter - * @param documentReferenceFactory - * @param stages - * @param converter - */ - constructor( - db: Firestore, - userDataReader: UserDataReader, - userDataWriter: AbstractUserDataWriter, - documentReferenceFactory: (id: DocumentKey) => DocumentReference, - stages: Stage[], - // TODO(pipeline) support converter - //private converter: FirestorePipelineConverter = defaultPipelineConverter() - converter: unknown = {} - ) { - super( - db, - userDataReader, - userDataWriter, - documentReferenceFactory, - stages, - converter - ); - } - - /** - * Executes this pipeline and returns a Promise to represent the asynchronous operation. - * - *

    The returned Promise can be used to track the progress of the pipeline execution - * and retrieve the results (or handle any errors) asynchronously. - * - *

    The pipeline results are returned as a list of {@link PipelineResult} objects. Each {@link - * PipelineResult} typically represents a single key/value map that has passed through all the - * stages of the pipeline, however this might differ depending on the stages involved in the - * pipeline. For example: - * - *

      - *
    • If there are no stages or only transformation stages, each {@link PipelineResult} - * represents a single document.
    • - *
    • If there is an aggregation, only a single {@link PipelineResult} is returned, - * representing the aggregated results over the entire dataset .
    • - *
    • If there is an aggregation stage with grouping, each {@link PipelineResult} represents a - * distinct group and its associated aggregated values.
    • - *
    - * - *

    Example: - * - * ```typescript - * const futureResults = await firestore.pipeline().collection("books") - * .where(gt(Field.of("rating"), 4.5)) - * .select("title", "author", "rating") - * .execute(); - * ``` - * - * @return A Promise representing the asynchronous pipeline execution. - */ - execute(): Promise>> { - const firestore = cast(this._db, Firestore); - const client = ensureFirestoreConfigured(firestore); - return firestoreClientExecutePipeline(client, this).then(result => { - const docs = result.map( - element => - new PipelineResult( - this.userDataWriter, - element.key?.path - ? this.documentReferenceFactory(element.key) - : undefined, - element.fields, - element.executionTime?.toTimestamp(), - element.createTime?.toTimestamp(), - element.updateTime?.toTimestamp() - //this.converter - ) - ); - - return docs; - }); - } -} - -/** - * Experimental Modular API for console testing. - * @param firestore - */ -export function pipeline(firestore: Firestore): PipelineSource; - -/** - * Experimental Modular API for console testing. - * @param query - */ -export function pipeline(query: Query): Pipeline; - -export function pipeline( - firestoreOrQuery: Firestore | Query -): PipelineSource | Pipeline { - return firestoreOrQuery.pipeline(); -} diff --git a/packages/firestore/src/api/reference_impl.ts b/packages/firestore/src/api/reference_impl.ts index 4283453d81d..8fa21a13e6d 100644 --- a/packages/firestore/src/api/reference_impl.ts +++ b/packages/firestore/src/api/reference_impl.ts @@ -37,6 +37,7 @@ import { } from '../core/firestore_client'; import { newQueryForPath, Query as InternalQuery } from '../core/query'; import { ViewSnapshot } from '../core/view_snapshot'; +import { Bytes } from '../lite-api/bytes'; import { FieldPath } from '../lite-api/field_path'; import { validateHasExplicitOrderByForLimitToLast } from '../lite-api/query'; import { @@ -58,9 +59,11 @@ import { parseUpdateData, parseUpdateVarargs } from '../lite-api/user_data_reader'; +import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; import { DocumentKey } from '../model/document_key'; import { DeleteMutation, Mutation, Precondition } from '../model/mutation'; import { debugAssert } from '../util/assert'; +import { ByteString } from '../util/byte_string'; import { Code, FirestoreError } from '../util/error'; import { cast } from '../util/input_validation'; @@ -71,7 +74,6 @@ import { QuerySnapshot, SnapshotMetadata } from './snapshot'; -import { ExpUserDataWriter } from './user_data_writer'; /** * An options object that can be passed to {@link (onSnapshot:1)} and {@link @@ -128,6 +130,21 @@ export function getDoc( ).then(snapshot => convertToDocSnapshot(firestore, reference, snapshot)); } +export class ExpUserDataWriter extends AbstractUserDataWriter { + constructor(protected firestore: Firestore) { + super(); + } + + protected convertBytes(bytes: ByteString): Bytes { + return new Bytes(bytes); + } + + protected convertReference(name: string): DocumentReference { + const key = this.convertDocumentKey(name, this.firestore._databaseId); + return new DocumentReference(this.firestore, /* converter= */ null, key); + } +} + /** * Reads the document referred to by this `DocumentReference` from cache. * Returns an error if the document is not currently cached. diff --git a/packages/firestore/src/api/transaction.ts b/packages/firestore/src/api/transaction.ts index 8f83f527182..955866f19b4 100644 --- a/packages/firestore/src/api/transaction.ts +++ b/packages/firestore/src/api/transaction.ts @@ -28,9 +28,9 @@ import { validateReference } from '../lite-api/write_batch'; import { cast } from '../util/input_validation'; import { ensureFirestoreConfigured, Firestore } from './database'; +import { ExpUserDataWriter } from './reference_impl'; import { DocumentSnapshot, SnapshotMetadata } from './snapshot'; import { TransactionOptions } from './transaction_options'; -import { ExpUserDataWriter } from './user_data_writer'; /** * A reference to a transaction. diff --git a/packages/firestore/src/api/user_data_writer.ts b/packages/firestore/src/api/user_data_writer.ts deleted file mode 100644 index 3567f72cd93..00000000000 --- a/packages/firestore/src/api/user_data_writer.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Bytes } from '../lite-api/bytes'; -import { DocumentReference } from '../lite-api/reference'; -import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; -import { ByteString } from '../util/byte_string'; - -import { Firestore } from './database'; - -export class ExpUserDataWriter extends AbstractUserDataWriter { - constructor(protected firestore: Firestore) { - super(); - } - - protected convertBytes(bytes: ByteString): Bytes { - return new Bytes(bytes); - } - - protected convertReference(name: string): DocumentReference { - const key = this.convertDocumentKey(name, this.firestore._databaseId); - return new DocumentReference(this.firestore, /* converter= */ null, key); - } -} diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 141f348d7bf..39bb8dd4eba 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -23,7 +23,6 @@ import { CredentialsProvider } from '../api/credentials'; import { User } from '../auth/user'; -import { Pipeline } from '../lite-api/pipeline'; import { LocalStore } from '../local/local_store'; import { localStoreConfigureFieldIndexes, @@ -39,16 +38,11 @@ import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { FieldIndex } from '../model/field_index'; import { Mutation } from '../model/mutation'; -import { PipelineStreamElement } from '../model/pipeline_stream_element'; import { toByteStreamReader } from '../platform/byte_stream_reader'; import { newSerializer } from '../platform/serializer'; import { newTextEncoder } from '../platform/text_serializer'; import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; -import { - Datastore, - invokeExecutePipeline, - invokeRunAggregationQueryRpc -} from '../remote/datastore'; +import { Datastore, invokeRunAggregationQueryRpc } from '../remote/datastore'; import { RemoteStore, remoteStoreDisableNetwork, @@ -237,23 +231,11 @@ export async function setOfflineComponentProvider( } }); - offlineComponentProvider.persistence.setDatabaseDeletedListener(() => { - logWarn('Terminating Firestore due to IndexedDb database deletion'); - client - .terminate() - .then(() => { - logDebug( - 'Terminating Firestore due to IndexedDb database deletion ' + - 'completed successfully' - ); - }) - .catch(error => { - logWarn( - 'Terminating Firestore due to IndexedDb database deletion failed', - error - ); - }); - }); + // When a user calls clearPersistence() in one client, all other clients + // need to be terminated to allow the delete to succeed. + offlineComponentProvider.persistence.setDatabaseDeletedListener(() => + client.terminate() + ); client._offlineComponents = offlineComponentProvider; } @@ -568,23 +550,6 @@ export function firestoreClientRunAggregateQuery( return deferred.promise; } -export function firestoreClientExecutePipeline( - client: FirestoreClient, - pipeline: Pipeline -): Promise { - const deferred = new Deferred(); - - client.asyncQueue.enqueueAndForget(async () => { - try { - const datastore = await getDatastore(client); - deferred.resolve(invokeExecutePipeline(datastore, pipeline)); - } catch (e) { - deferred.reject(e as Error); - } - }); - return deferred.promise; -} - export function firestoreClientWrite( client: FirestoreClient, mutations: Mutation[] diff --git a/packages/firestore/src/core/pipeline-util.ts b/packages/firestore/src/core/pipeline-util.ts deleted file mode 100644 index 6ff966cb754..00000000000 --- a/packages/firestore/src/core/pipeline-util.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Constant, - Expr, - Field, - FilterCondition, - not, - andFunction, - orFunction -} from '../lite-api/expressions'; -import { isNanValue, isNullValue } from '../model/values'; -import { - ArrayValue as ProtoArrayValue, - Function as ProtoFunction, - LatLng as ProtoLatLng, - MapValue as ProtoMapValue, - Pipeline as ProtoPipeline, - Timestamp as ProtoTimestamp, - Value as ProtoValue -} from '../protos/firestore_proto_api'; -import { fail } from '../util/assert'; -import { isPlainObject } from '../util/input_validation'; - -import { - CompositeFilter as CompositeFilterInternal, - CompositeOperator, - FieldFilter as FieldFilterInternal, - Filter as FilterInternal, - Operator -} from './filter'; - -/* eslint @typescript-eslint/no-explicit-any: 0 */ - -function isITimestamp(obj: any): obj is ProtoTimestamp { - if (typeof obj !== 'object' || obj === null) { - return false; // Must be a non-null object - } - if ( - 'seconds' in obj && - (obj.seconds === null || - typeof obj.seconds === 'number' || - typeof obj.seconds === 'string') && - 'nanos' in obj && - (obj.nanos === null || typeof obj.nanos === 'number') - ) { - return true; - } - - return false; -} -function isILatLng(obj: any): obj is ProtoLatLng { - if (typeof obj !== 'object' || obj === null) { - return false; // Must be a non-null object - } - if ( - 'latitude' in obj && - (obj.latitude === null || typeof obj.latitude === 'number') && - 'longitude' in obj && - (obj.longitude === null || typeof obj.longitude === 'number') - ) { - return true; - } - - return false; -} -function isIArrayValue(obj: any): obj is ProtoArrayValue { - if (typeof obj !== 'object' || obj === null) { - return false; // Must be a non-null object - } - if ('values' in obj && (obj.values === null || Array.isArray(obj.values))) { - return true; - } - - return false; -} -function isIMapValue(obj: any): obj is ProtoMapValue { - if (typeof obj !== 'object' || obj === null) { - return false; // Must be a non-null object - } - if ('fields' in obj && (obj.fields === null || isPlainObject(obj.fields))) { - return true; - } - - return false; -} -function isIFunction(obj: any): obj is ProtoFunction { - if (typeof obj !== 'object' || obj === null) { - return false; // Must be a non-null object - } - if ( - 'name' in obj && - (obj.name === null || typeof obj.name === 'string') && - 'args' in obj && - (obj.args === null || Array.isArray(obj.args)) - ) { - return true; - } - - return false; -} - -function isIPipeline(obj: any): obj is ProtoPipeline { - if (typeof obj !== 'object' || obj === null) { - return false; // Must be a non-null object - } - if ('stages' in obj && (obj.stages === null || Array.isArray(obj.stages))) { - return true; - } - - return false; -} - -export function isFirestoreValue(obj: any): obj is ProtoValue { - if (typeof obj !== 'object' || obj === null) { - return false; // Must be a non-null object - } - - // Check optional properties and their types - if ( - ('nullValue' in obj && - (obj.nullValue === null || obj.nullValue === 'NULL_VALUE')) || - ('booleanValue' in obj && - (obj.booleanValue === null || typeof obj.booleanValue === 'boolean')) || - ('integerValue' in obj && - (obj.integerValue === null || - typeof obj.integerValue === 'number' || - typeof obj.integerValue === 'string')) || - ('doubleValue' in obj && - (obj.doubleValue === null || typeof obj.doubleValue === 'number')) || - ('timestampValue' in obj && - (obj.timestampValue === null || isITimestamp(obj.timestampValue))) || - ('stringValue' in obj && - (obj.stringValue === null || typeof obj.stringValue === 'string')) || - ('bytesValue' in obj && - (obj.bytesValue === null || obj.bytesValue instanceof Uint8Array)) || - ('referenceValue' in obj && - (obj.referenceValue === null || - typeof obj.referenceValue === 'string')) || - ('geoPointValue' in obj && - (obj.geoPointValue === null || isILatLng(obj.geoPointValue))) || - ('arrayValue' in obj && - (obj.arrayValue === null || isIArrayValue(obj.arrayValue))) || - ('mapValue' in obj && - (obj.mapValue === null || isIMapValue(obj.mapValue))) || - ('fieldReferenceValue' in obj && - (obj.fieldReferenceValue === null || - typeof obj.fieldReferenceValue === 'string')) || - ('functionValue' in obj && - (obj.functionValue === null || isIFunction(obj.functionValue))) || - ('pipelineValue' in obj && - (obj.pipelineValue === null || isIPipeline(obj.pipelineValue))) - ) { - return true; - } - - return false; -} - -export function toPipelineFilterCondition( - f: FilterInternal -): FilterCondition & Expr { - if (f instanceof FieldFilterInternal) { - const field = Field.of(f.field.toString()); - if (isNanValue(f.value)) { - if (f.op === Operator.EQUAL) { - return andFunction(field.exists(), field.isNaN()); - } else { - return andFunction(field.exists(), not(field.isNaN())); - } - } else if (isNullValue(f.value)) { - if (f.op === Operator.EQUAL) { - return andFunction(field.exists(), field.eq(null)); - } else { - return andFunction(field.exists(), not(field.eq(null))); - } - } else { - // Comparison filters - const value = f.value; - switch (f.op) { - case Operator.LESS_THAN: - return andFunction(field.exists(), field.lt(value)); - case Operator.LESS_THAN_OR_EQUAL: - return andFunction(field.exists(), field.lte(value)); - case Operator.GREATER_THAN: - return andFunction(field.exists(), field.gt(value)); - case Operator.GREATER_THAN_OR_EQUAL: - return andFunction(field.exists(), field.gte(value)); - case Operator.EQUAL: - return andFunction(field.exists(), field.eq(value)); - case Operator.NOT_EQUAL: - return andFunction(field.exists(), field.neq(value)); - case Operator.ARRAY_CONTAINS: - return andFunction(field.exists(), field.arrayContains(value)); - case Operator.IN: { - const values = value?.arrayValue?.values?.map((val: any) => - Constant.of(val) - ); - return andFunction(field.exists(), field.in(...values!)); - } - case Operator.ARRAY_CONTAINS_ANY: { - const values = value?.arrayValue?.values?.map((val: any) => - Constant.of(val) - ); - return andFunction(field.exists(), field.arrayContainsAny(values!)); - } - case Operator.NOT_IN: { - const values = value?.arrayValue?.values?.map((val: any) => - Constant.of(val) - ); - return andFunction(field.exists(), not(field.in(...values!))); - } - default: - fail(0x5cb4, 'Unexpected operator'); - } - } - } else if (f instanceof CompositeFilterInternal) { - switch (f.op) { - case CompositeOperator.AND: { - const conditions = f - .getFilters() - .map(f => toPipelineFilterCondition(f)); - return andFunction(conditions[0], ...conditions.slice(1)); - } - case CompositeOperator.OR: { - const conditions = f - .getFilters() - .map(f => toPipelineFilterCondition(f)); - return orFunction(conditions[0], ...conditions.slice(1)); - } - default: - fail(0xd914, 'Unexpected operator'); - } - } - - throw new Error(`Failed to convert filter to pipeline conditions: ${f}`); -} diff --git a/packages/firestore/src/core/transaction_runner.ts b/packages/firestore/src/core/transaction_runner.ts index d9e679321b5..5b2fc5819f7 100644 --- a/packages/firestore/src/core/transaction_runner.ts +++ b/packages/firestore/src/core/transaction_runner.ts @@ -112,8 +112,8 @@ export class TransactionRunner { } } - private isRetryableTransactionError(error: Error): boolean { - if (error.name === 'FirebaseError') { + private isRetryableTransactionError(error: Error | undefined): boolean { + if (error?.name === 'FirebaseError') { // In transactions, the backend will fail outdated reads with FAILED_PRECONDITION and // non-matching document versions with ABORTED. These errors should be retried. const code = (error as FirestoreError).code; diff --git a/packages/firestore/src/lite-api/database.ts b/packages/firestore/src/lite-api/database.ts index 595d4a8b7a3..6af324e4ba4 100644 --- a/packages/firestore/src/lite-api/database.ts +++ b/packages/firestore/src/lite-api/database.ts @@ -45,9 +45,6 @@ import { cast } from '../util/input_validation'; import { logWarn } from '../util/log'; import { FirestoreService, removeComponents } from './components'; -// `import type` to avoid bundling the source for -// pipelines if `useFirestorePipelines()` is not called -import type { PipelineSource } from './pipeline-source'; import { DEFAULT_HOST, FirestoreSettingsImpl, @@ -189,15 +186,6 @@ export class Firestore implements FirestoreService { removeComponents(this); return Promise.resolve(); } - - /** - * Pipeline query. - */ - pipeline(): PipelineSource { - throw new Error( - 'Pipelines not initialized. Your application must call `useFirestorePipelines()` before using Firestore Pipeline features.' - ); - } } /** diff --git a/packages/firestore/src/lite-api/database_augmentation.ts b/packages/firestore/src/lite-api/database_augmentation.ts deleted file mode 100644 index 14b9cf101c5..00000000000 --- a/packages/firestore/src/lite-api/database_augmentation.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DocumentKey } from '../model/document_key'; - -import { Firestore } from './database'; -import { Pipeline } from './pipeline'; -import { PipelineSource } from './pipeline-source'; -import { DocumentReference, Query } from './reference'; -import { LiteUserDataWriter } from './reference_impl'; -import { newUserDataReader } from './user_data_reader'; - -export function useFirestorePipelines(): void { - Firestore.prototype.pipeline = function (): PipelineSource { - const userDataWriter = new LiteUserDataWriter(this); - const userDataReader = newUserDataReader(this); - return new PipelineSource( - this, - userDataReader, - userDataWriter, - (key: DocumentKey) => { - return new DocumentReference(this, null, key); - } - ); - }; - - Query.prototype.pipeline = function (): Pipeline { - let pipeline; - if (this._query.collectionGroup) { - pipeline = this.firestore - .pipeline() - .collectionGroup(this._query.collectionGroup); - } else { - pipeline = this.firestore - .pipeline() - .collection(this._query.path.canonicalString()); - } - - // TODO(pipeline) convert existing query filters, limits, etc into - // pipeline stages - - return pipeline; - }; -} diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts deleted file mode 100644 index 791351c221b..00000000000 --- a/packages/firestore/src/lite-api/expressions.ts +++ /dev/null @@ -1,6739 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* eslint @typescript-eslint/no-explicit-any: 0 */ - -import { - DOCUMENT_KEY_NAME, - FieldPath as InternalFieldPath -} from '../model/path'; -import { Value as ProtoValue } from '../protos/firestore_proto_api'; -import { - JsonProtoSerializer, - ProtoSerializable, - toStringValue, - UserData -} from '../remote/serializer'; -import { hardAssert } from '../util/assert'; - -import { documentId, FieldPath } from './field_path'; -import { GeoPoint } from './geo_point'; -import { Pipeline } from './pipeline'; -import { DocumentReference } from './reference'; -import { Timestamp } from './timestamp'; -import { - fieldPathFromArgument, - parseData, - UserDataReader, - UserDataSource -} from './user_data_reader'; -import { VectorValue } from './vector_value'; - -/** - * @beta - * - * An interface that represents a selectable expression. - */ -export interface Selectable { - selectable: true; -} - -/** - * @beta - * - * An interface that represents a filter condition. - */ -export interface FilterCondition { - filterable: true; -} - -/** - * @beta - * - * An interface that represents an accumulator. - */ -export interface Accumulator { - accumulator: true; - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue; -} - -/** - * @beta - * - * An accumulator target, which is an expression with an alias that also implements the Accumulator interface. - */ -export type AccumulatorTarget = ExprWithAlias; - -/** - * @beta - * - * A filter expression, which is an expression that also implements the FilterCondition interface. - */ -export type FilterExpr = Expr & FilterCondition; - -/** - * @beta - * - * A selectable expression, which is an expression that also implements the Selectable interface. - */ -export type SelectableExpr = Expr & Selectable; - -/** - * @beta - * - * An enumeration of the different types of expressions. - */ -export type ExprType = - | 'Field' - | 'Constant' - | 'Function' - | 'ListOfExprs' - | 'ExprWithAlias'; - -/** - * @beta - * - * Represents an expression that can be evaluated to a value within the execution of a {@link - * Pipeline}. - * - * Expressions are the building blocks for creating complex queries and transformations in - * Firestore pipelines. They can represent: - * - * - **Field references:** Access values from document fields. - * - **Literals:** Represent constant values (strings, numbers, booleans). - * - **Function calls:** Apply functions to one or more expressions. - * - **Aggregations:** Calculate aggregate values (e.g., sum, average) over a set of documents. - * - * The `Expr` class provides a fluent API for building expressions. You can chain together - * method calls to create complex expressions. - */ -export abstract class Expr implements ProtoSerializable, UserData { - /** - * Creates an expression that adds this expression to another expression. - * - * ```typescript - * // Add the value of the 'quantity' field and the 'reserve' field. - * Field.of("quantity").add(Field.of("reserve")); - * ``` - * - * @param other The expression to add to this expression. - * @return A new `Expr` representing the addition operation. - */ - add(other: Expr): Add; - - /** - * Creates an expression that adds this expression to a constant value. - * - * ```typescript - * // Add 5 to the value of the 'age' field - * Field.of("age").add(5); - * ``` - * - * @param other The constant value to add. - * @return A new `Expr` representing the addition operation. - */ - add(other: any): Add; - add(other: any): Add { - if (other instanceof Expr) { - return new Add(this, other); - } - return new Add(this, Constant.of(other)); - } - - /** - * Creates an expression that subtracts another expression from this expression. - * - * ```typescript - * // Subtract the 'discount' field from the 'price' field - * Field.of("price").subtract(Field.of("discount")); - * ``` - * - * @param other The expression to subtract from this expression. - * @return A new `Expr` representing the subtraction operation. - */ - subtract(other: Expr): Subtract; - - /** - * Creates an expression that subtracts a constant value from this expression. - * - * ```typescript - * // Subtract 20 from the value of the 'total' field - * Field.of("total").subtract(20); - * ``` - * - * @param other The constant value to subtract. - * @return A new `Expr` representing the subtraction operation. - */ - subtract(other: any): Subtract; - subtract(other: any): Subtract { - if (other instanceof Expr) { - return new Subtract(this, other); - } - return new Subtract(this, Constant.of(other)); - } - - /** - * Creates an expression that multiplies this expression by another expression. - * - * ```typescript - * // Multiply the 'quantity' field by the 'price' field - * Field.of("quantity").multiply(Field.of("price")); - * ``` - * - * @param other The expression to multiply by. - * @return A new `Expr` representing the multiplication operation. - */ - multiply(other: Expr): Multiply; - - /** - * Creates an expression that multiplies this expression by a constant value. - * - * ```typescript - * // Multiply the 'value' field by 2 - * Field.of("value").multiply(2); - * ``` - * - * @param other The constant value to multiply by. - * @return A new `Expr` representing the multiplication operation. - */ - multiply(other: any): Multiply; - multiply(other: any): Multiply { - if (other instanceof Expr) { - return new Multiply(this, other); - } - return new Multiply(this, Constant.of(other)); - } - - /** - * Creates an expression that divides this expression by another expression. - * - * ```typescript - * // Divide the 'total' field by the 'count' field - * Field.of("total").divide(Field.of("count")); - * ``` - * - * @param other The expression to divide by. - * @return A new `Expr` representing the division operation. - */ - divide(other: Expr): Divide; - - /** - * Creates an expression that divides this expression by a constant value. - * - * ```typescript - * // Divide the 'value' field by 10 - * Field.of("value").divide(10); - * ``` - * - * @param other The constant value to divide by. - * @return A new `Expr` representing the division operation. - */ - divide(other: any): Divide; - divide(other: any): Divide { - if (other instanceof Expr) { - return new Divide(this, other); - } - return new Divide(this, Constant.of(other)); - } - - /** - * Creates an expression that calculates the modulo (remainder) of dividing this expression by another expression. - * - * ```typescript - * // Calculate the remainder of dividing the 'value' field by the 'divisor' field - * Field.of("value").mod(Field.of("divisor")); - * ``` - * - * @param other The expression to divide by. - * @return A new `Expr` representing the modulo operation. - */ - mod(other: Expr): Mod; - - /** - * Creates an expression that calculates the modulo (remainder) of dividing this expression by a constant value. - * - * ```typescript - * // Calculate the remainder of dividing the 'value' field by 10 - * Field.of("value").mod(10); - * ``` - * - * @param other The constant value to divide by. - * @return A new `Expr` representing the modulo operation. - */ - mod(other: any): Mod; - mod(other: any): Mod { - if (other instanceof Expr) { - return new Mod(this, other); - } - return new Mod(this, Constant.of(other)); - } - - // /** - // * Creates an expression that applies a bitwise AND operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise AND of 'field1' and 'field2'. - // * Field.of("field1").bitAnd(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression. - // * @return A new {@code Expr} representing the bitwise AND operation. - // */ - // bitAnd(other: Expr): BitAnd; - // - // /** - // * Creates an expression that applies a bitwise AND operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise AND of 'field1' and 0xFF. - // * Field.of("field1").bitAnd(0xFF); - // * ``` - // * - // * @param other The right operand constant. - // * @return A new {@code Expr} representing the bitwise AND operation. - // */ - // bitAnd(other: any): BitAnd; - // bitAnd(other: any): BitAnd { - // if (other instanceof Expr) { - // return new BitAnd(this, other); - // } - // return new BitAnd(this, Constant.of(other)); - // } - // - // /** - // * Creates an expression that applies a bitwise OR operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise OR of 'field1' and 'field2'. - // * Field.of("field1").bitOr(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression. - // * @return A new {@code Expr} representing the bitwise OR operation. - // */ - // bitOr(other: Expr): BitOr; - // - // /** - // * Creates an expression that applies a bitwise OR operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise OR of 'field1' and 0xFF. - // * Field.of("field1").bitOr(0xFF); - // * ``` - // * - // * @param other The right operand constant. - // * @return A new {@code Expr} representing the bitwise OR operation. - // */ - // bitOr(other: any): BitOr; - // bitOr(other: any): BitOr { - // if (other instanceof Expr) { - // return new BitOr(this, other); - // } - // return new BitOr(this, Constant.of(other)); - // } - // - // /** - // * Creates an expression that applies a bitwise XOR operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise XOR of 'field1' and 'field2'. - // * Field.of("field1").bitXor(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression. - // * @return A new {@code Expr} representing the bitwise XOR operation. - // */ - // bitXor(other: Expr): BitXor; - // - // /** - // * Creates an expression that applies a bitwise XOR operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise XOR of 'field1' and 0xFF. - // * Field.of("field1").bitXor(0xFF); - // * ``` - // * - // * @param other The right operand constant. - // * @return A new {@code Expr} representing the bitwise XOR operation. - // */ - // bitXor(other: any): BitXor; - // bitXor(other: any): BitXor { - // if (other instanceof Expr) { - // return new BitXor(this, other); - // } - // return new BitXor(this, Constant.of(other)); - // } - // - // /** - // * Creates an expression that applies a bitwise NOT operation to this expression. - // * - // * ```typescript - // * // Calculate the bitwise NOT of 'field1'. - // * Field.of("field1").bitNot(); - // * ``` - // * - // * @return A new {@code Expr} representing the bitwise NOT operation. - // */ - // bitNot(): BitNot { - // return new BitNot(this); - // } - // - // /** - // * Creates an expression that applies a bitwise left shift operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise left shift of 'field1' by 'field2' bits. - // * Field.of("field1").bitLeftShift(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression representing the number of bits to shift. - // * @return A new {@code Expr} representing the bitwise left shift operation. - // */ - // bitLeftShift(other: Expr): BitLeftShift; - // - // /** - // * Creates an expression that applies a bitwise left shift operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise left shift of 'field1' by 2 bits. - // * Field.of("field1").bitLeftShift(2); - // * ``` - // * - // * @param other The right operand constant representing the number of bits to shift. - // * @return A new {@code Expr} representing the bitwise left shift operation. - // */ - // bitLeftShift(other: number): BitLeftShift; - // bitLeftShift(other: Expr | number): BitLeftShift { - // if (typeof other === 'number') { - // return new BitLeftShift(this, Constant.of(other)); - // } - // return new BitLeftShift(this, other as Expr); - // } - // - // /** - // * Creates an expression that applies a bitwise right shift operation between this expression and another expression. - // * - // * ```typescript - // * // Calculate the bitwise right shift of 'field1' by 'field2' bits. - // * Field.of("field1").bitRightShift(Field.of("field2")); - // * ``` - // * - // * @param other The right operand expression representing the number of bits to shift. - // * @return A new {@code Expr} representing the bitwise right shift operation. - // */ - // bitRightShift(other: Expr): BitRightShift; - // - // /** - // * Creates an expression that applies a bitwise right shift operation between this expression and a constant value. - // * - // * ```typescript - // * // Calculate the bitwise right shift of 'field1' by 2 bits. - // * Field.of("field1").bitRightShift(2); - // * ``` - // * - // * @param other The right operand constant representing the number of bits to shift. - // * @return A new {@code Expr} representing the bitwise right shift operation. - // */ - // bitRightShift(other: number): BitRightShift; - // bitRightShift(other: Expr | number): BitRightShift { - // if (typeof other === 'number') { - // return new BitRightShift(this, Constant.of(other)); - // } - // return new BitRightShift(this, other as Expr); - // } - - /** - * Creates an expression that checks if this expression is equal to another expression. - * - * ```typescript - * // Check if the 'age' field is equal to 21 - * Field.of("age").eq(21); - * ``` - * - * @param other The expression to compare for equality. - * @return A new `Expr` representing the equality comparison. - */ - eq(other: Expr): Eq; - - /** - * Creates an expression that checks if this expression is equal to a constant value. - * - * ```typescript - * // Check if the 'city' field is equal to "London" - * Field.of("city").eq("London"); - * ``` - * - * @param other The constant value to compare for equality. - * @return A new `Expr` representing the equality comparison. - */ - eq(other: any): Eq; - eq(other: any): Eq { - if (other instanceof Expr) { - return new Eq(this, other); - } - return new Eq(this, Constant.of(other)); - } - - /** - * Creates an expression that checks if this expression is not equal to another expression. - * - * ```typescript - * // Check if the 'status' field is not equal to "completed" - * Field.of("status").neq("completed"); - * ``` - * - * @param other The expression to compare for inequality. - * @return A new `Expr` representing the inequality comparison. - */ - neq(other: Expr): Neq; - - /** - * Creates an expression that checks if this expression is not equal to a constant value. - * - * ```typescript - * // Check if the 'country' field is not equal to "USA" - * Field.of("country").neq("USA"); - * ``` - * - * @param other The constant value to compare for inequality. - * @return A new `Expr` representing the inequality comparison. - */ - neq(other: any): Neq; - neq(other: any): Neq { - if (other instanceof Expr) { - return new Neq(this, other); - } - return new Neq(this, Constant.of(other)); - } - - /** - * Creates an expression that checks if this expression is less than another expression. - * - * ```typescript - * // Check if the 'age' field is less than 'limit' - * Field.of("age").lt(Field.of('limit')); - * ``` - * - * @param other The expression to compare for less than. - * @return A new `Expr` representing the less than comparison. - */ - lt(other: Expr): Lt; - - /** - * Creates an expression that checks if this expression is less than a constant value. - * - * ```typescript - * // Check if the 'price' field is less than 50 - * Field.of("price").lt(50); - * ``` - * - * @param other The constant value to compare for less than. - * @return A new `Expr` representing the less than comparison. - */ - lt(other: any): Lt; - lt(other: any): Lt { - if (other instanceof Expr) { - return new Lt(this, other); - } - return new Lt(this, Constant.of(other)); - } - - /** - * Creates an expression that checks if this expression is less than or equal to another - * expression. - * - * ```typescript - * // Check if the 'quantity' field is less than or equal to 20 - * Field.of("quantity").lte(Constant.of(20)); - * ``` - * - * @param other The expression to compare for less than or equal to. - * @return A new `Expr` representing the less than or equal to comparison. - */ - lte(other: Expr): Lte; - - /** - * Creates an expression that checks if this expression is less than or equal to a constant value. - * - * ```typescript - * // Check if the 'score' field is less than or equal to 70 - * Field.of("score").lte(70); - * ``` - * - * @param other The constant value to compare for less than or equal to. - * @return A new `Expr` representing the less than or equal to comparison. - */ - lte(other: any): Lte; - lte(other: any): Lte { - if (other instanceof Expr) { - return new Lte(this, other); - } - return new Lte(this, Constant.of(other)); - } - - /** - * Creates an expression that checks if this expression is greater than another expression. - * - * ```typescript - * // Check if the 'age' field is greater than the 'limit' field - * Field.of("age").gt(Field.of("limit")); - * ``` - * - * @param other The expression to compare for greater than. - * @return A new `Expr` representing the greater than comparison. - */ - gt(other: Expr): Gt; - - /** - * Creates an expression that checks if this expression is greater than a constant value. - * - * ```typescript - * // Check if the 'price' field is greater than 100 - * Field.of("price").gt(100); - * ``` - * - * @param other The constant value to compare for greater than. - * @return A new `Expr` representing the greater than comparison. - */ - gt(other: any): Gt; - gt(other: any): Gt { - if (other instanceof Expr) { - return new Gt(this, other); - } - return new Gt(this, Constant.of(other)); - } - - /** - * Creates an expression that checks if this expression is greater than or equal to another - * expression. - * - * ```typescript - * // Check if the 'quantity' field is greater than or equal to field 'requirement' plus 1 - * Field.of("quantity").gte(Field.of('requirement').add(1)); - * ``` - * - * @param other The expression to compare for greater than or equal to. - * @return A new `Expr` representing the greater than or equal to comparison. - */ - gte(other: Expr): Gte; - - /** - * Creates an expression that checks if this expression is greater than or equal to a constant - * value. - * - * ```typescript - * // Check if the 'score' field is greater than or equal to 80 - * Field.of("score").gte(80); - * ``` - * - * @param other The constant value to compare for greater than or equal to. - * @return A new `Expr` representing the greater than or equal to comparison. - */ - gte(other: any): Gte; - gte(other: any): Gte { - if (other instanceof Expr) { - return new Gte(this, other); - } - return new Gte(this, Constant.of(other)); - } - - /** - * Creates an expression that concatenates an array expression with one or more other arrays. - * - * ```typescript - * // Combine the 'items' array with another array field. - * Field.of("items").arrayConcat(Field.of("otherItems")); - * ``` - * - * @param arrays The array expressions to concatenate. - * @return A new `Expr` representing the concatenated array. - */ - arrayConcat(arrays: Expr[]): ArrayConcat; - - /** - * Creates an expression that concatenates an array expression with one or more other arrays. - * - * ```typescript - * // Combine the 'tags' array with a new array and an array field - * Field.of("tags").arrayConcat(Arrays.asList("newTag1", "newTag2"), Field.of("otherTag")); - * ``` - * - * @param arrays The array expressions or values to concatenate. - * @return A new `Expr` representing the concatenated array. - */ - arrayConcat(arrays: any[]): ArrayConcat; - arrayConcat(arrays: any[]): ArrayConcat { - const exprValues = arrays.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayConcat(this, exprValues); - } - - /** - * Creates an expression that checks if an array contains a specific element. - * - * ```typescript - * // Check if the 'sizes' array contains the value from the 'selectedSize' field - * Field.of("sizes").arrayContains(Field.of("selectedSize")); - * ``` - * - * @param element The element to search for in the array. - * @return A new `Expr` representing the 'array_contains' comparison. - */ - arrayContains(element: Expr): ArrayContains; - - /** - * Creates an expression that checks if an array contains a specific value. - * - * ```typescript - * // Check if the 'colors' array contains "red" - * Field.of("colors").arrayContains("red"); - * ``` - * - * @param element The element to search for in the array. - * @return A new `Expr` representing the 'array_contains' comparison. - */ - arrayContains(element: any): ArrayContains; - arrayContains(element: any): ArrayContains { - if (element instanceof Expr) { - return new ArrayContains(this, element); - } - return new ArrayContains(this, Constant.of(element)); - } - - /** - * Creates an expression that checks if an array contains all the specified elements. - * - * ```typescript - * // Check if the 'tags' array contains both "news" and "sports" - * Field.of("tags").arrayContainsAll(Field.of("tag1"), Field.of("tag2")); - * ``` - * - * @param values The elements to check for in the array. - * @return A new `Expr` representing the 'array_contains_all' comparison. - */ - arrayContainsAll(...values: Expr[]): ArrayContainsAll; - - /** - * Creates an expression that checks if an array contains all the specified elements. - * - * ```typescript - * // Check if the 'tags' array contains both of the values from field 'tag1' and "tag2" - * Field.of("tags").arrayContainsAll(Field.of("tag1"), Field.of("tag2")); - * ``` - * - * @param values The elements to check for in the array. - * @return A new `Expr` representing the 'array_contains_all' comparison. - */ - arrayContainsAll(...values: any[]): ArrayContainsAll; - arrayContainsAll(...values: any[]): ArrayContainsAll { - const exprValues = values.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayContainsAll(this, exprValues); - } - - /** - * Creates an expression that checks if an array contains any of the specified elements. - * - * ```typescript - * // Check if the 'categories' array contains either values from field "cate1" or "cate2" - * Field.of("categories").arrayContainsAny(Field.of("cate1"), Field.of("cate2")); - * ``` - * - * @param values The elements to check for in the array. - * @return A new `Expr` representing the 'array_contains_any' comparison. - */ - arrayContainsAny(...values: Expr[]): ArrayContainsAny; - - /** - * Creates an expression that checks if an array contains any of the specified elements. - * - * ```typescript - * // Check if the 'groups' array contains either the value from the 'userGroup' field - * // or the value "guest" - * Field.of("groups").arrayContainsAny(Field.of("userGroup"), "guest"); - * ``` - * - * @param values The elements to check for in the array. - * @return A new `Expr` representing the 'array_contains_any' comparison. - */ - arrayContainsAny(...values: any[]): ArrayContainsAny; - arrayContainsAny(...values: any[]): ArrayContainsAny { - const exprValues = values.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayContainsAny(this, exprValues); - } - - /** - * Creates an expression that calculates the length of an array. - * - * ```typescript - * // Get the number of items in the 'cart' array - * Field.of("cart").arrayLength(); - * ``` - * - * @return A new `Expr` representing the length of the array. - */ - arrayLength(): ArrayLength { - return new ArrayLength(this); - } - - /** - * Creates an expression that checks if this expression is equal to any of the provided values or - * expressions. - * - * ```typescript - * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * Field.of("category").in("Electronics", Field.of("primaryType")); - * ``` - * - * @param others The values or expressions to check against. - * @return A new `Expr` representing the 'IN' comparison. - */ - in(...others: Expr[]): In; - - /** - * Creates an expression that checks if this expression is equal to any of the provided values or - * expressions. - * - * ```typescript - * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * Field.of("category").in("Electronics", Field.of("primaryType")); - * ``` - * - * @param others The values or expressions to check against. - * @return A new `Expr` representing the 'IN' comparison. - */ - in(...others: any[]): In; - in(...others: any[]): In { - const exprOthers = others.map(other => - other instanceof Expr ? other : Constant.of(other) - ); - return new In(this, exprOthers); - } - - /** - * Creates an expression that checks if this expression evaluates to 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NaN - * Field.of("value").divide(0).isNaN(); - * ``` - * - * @return A new `Expr` representing the 'isNaN' check. - */ - isNaN(): IsNan { - return new IsNan(this); - } - - /** - * Creates an expression that checks if a field exists in the document. - * - * ```typescript - * // Check if the document has a field named "phoneNumber" - * Field.of("phoneNumber").exists(); - * ``` - * - * @return A new `Expr` representing the 'exists' check. - */ - exists(): Exists { - return new Exists(this); - } - - /** - * Creates an expression that calculates the character length of a string in UTF-8. - * - * ```typescript - * // Get the character length of the 'name' field in its UTF-8 form. - * Field.of("name").charLength(); - * ``` - * - * @return A new `Expr` representing the length of the string. - */ - charLength(): CharLength { - return new CharLength(this); - } - - /** - * Creates an expression that performs a case-sensitive string comparison. - * - * ```typescript - * // Check if the 'title' field contains the word "guide" (case-sensitive) - * Field.of("title").like("%guide%"); - * ``` - * - * @param pattern The pattern to search for. You can use "%" as a wildcard character. - * @return A new `Expr` representing the 'like' comparison. - */ - like(pattern: string): Like; - - /** - * Creates an expression that performs a case-sensitive string comparison. - * - * ```typescript - * // Check if the 'title' field contains the word "guide" (case-sensitive) - * Field.of("title").like("%guide%"); - * ``` - * - * @param pattern The pattern to search for. You can use "%" as a wildcard character. - * @return A new `Expr` representing the 'like' comparison. - */ - like(pattern: Expr): Like; - like(stringOrExpr: string | Expr): Like { - if (typeof stringOrExpr === 'string') { - return new Like(this, Constant.of(stringOrExpr)); - } - return new Like(this, stringOrExpr as Expr); - } - - /** - * Creates an expression that checks if a string contains a specified regular expression as a - * substring. - * - * ```typescript - * // Check if the 'description' field contains "example" (case-insensitive) - * Field.of("description").regexContains("(?i)example"); - * ``` - * - * @param pattern The regular expression to use for the search. - * @return A new `Expr` representing the 'contains' comparison. - */ - regexContains(pattern: string): RegexContains; - - /** - * Creates an expression that checks if a string contains a specified regular expression as a - * substring. - * - * ```typescript - * // Check if the 'description' field contains the regular expression stored in field 'regex' - * Field.of("description").regexContains(Field.of("regex")); - * ``` - * - * @param pattern The regular expression to use for the search. - * @return A new `Expr` representing the 'contains' comparison. - */ - regexContains(pattern: Expr): RegexContains; - regexContains(stringOrExpr: string | Expr): RegexContains { - if (typeof stringOrExpr === 'string') { - return new RegexContains(this, Constant.of(stringOrExpr)); - } - return new RegexContains(this, stringOrExpr as Expr); - } - - /** - * Creates an expression that checks if a string matches a specified regular expression. - * - * ```typescript - * // Check if the 'email' field matches a valid email pattern - * Field.of("email").regexMatch("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); - * ``` - * - * @param pattern The regular expression to use for the match. - * @return A new `Expr` representing the regular expression match. - */ - regexMatch(pattern: string): RegexMatch; - - /** - * Creates an expression that checks if a string matches a specified regular expression. - * - * ```typescript - * // Check if the 'email' field matches a regular expression stored in field 'regex' - * Field.of("email").regexMatch(Field.of("regex")); - * ``` - * - * @param pattern The regular expression to use for the match. - * @return A new `Expr` representing the regular expression match. - */ - regexMatch(pattern: Expr): RegexMatch; - regexMatch(stringOrExpr: string | Expr): RegexMatch { - if (typeof stringOrExpr === 'string') { - return new RegexMatch(this, Constant.of(stringOrExpr)); - } - return new RegexMatch(this, stringOrExpr as Expr); - } - - /** - * Creates an expression that checks if a string contains a specified substring. - * - * ```typescript - * // Check if the 'description' field contains "example". - * Field.of("description").strContains("example"); - * ``` - * - * @param substring The substring to search for. - * @return A new `Expr` representing the 'contains' comparison. - */ - strContains(substring: string): StrContains; - - /** - * Creates an expression that checks if a string contains the string represented by another expression. - * - * ```typescript - * // Check if the 'description' field contains the value of the 'keyword' field. - * Field.of("description").strContains(Field.of("keyword")); - * ``` - * - * @param expr The expression representing the substring to search for. - * @return A new `Expr` representing the 'contains' comparison. - */ - strContains(expr: Expr): StrContains; - strContains(stringOrExpr: string | Expr): StrContains { - if (typeof stringOrExpr === 'string') { - return new StrContains(this, Constant.of(stringOrExpr)); - } - return new StrContains(this, stringOrExpr as Expr); - } - - /** - * Creates an expression that checks if a string starts with a given prefix. - * - * ```typescript - * // Check if the 'name' field starts with "Mr." - * Field.of("name").startsWith("Mr."); - * ``` - * - * @param prefix The prefix to check for. - * @return A new `Expr` representing the 'starts with' comparison. - */ - startsWith(prefix: string): StartsWith; - - /** - * Creates an expression that checks if a string starts with a given prefix (represented as an - * expression). - * - * ```typescript - * // Check if the 'fullName' field starts with the value of the 'firstName' field - * Field.of("fullName").startsWith(Field.of("firstName")); - * ``` - * - * @param prefix The prefix expression to check for. - * @return A new `Expr` representing the 'starts with' comparison. - */ - startsWith(prefix: Expr): StartsWith; - startsWith(stringOrExpr: string | Expr): StartsWith { - if (typeof stringOrExpr === 'string') { - return new StartsWith(this, Constant.of(stringOrExpr)); - } - return new StartsWith(this, stringOrExpr as Expr); - } - - /** - * Creates an expression that checks if a string ends with a given postfix. - * - * ```typescript - * // Check if the 'filename' field ends with ".txt" - * Field.of("filename").endsWith(".txt"); - * ``` - * - * @param suffix The postfix to check for. - * @return A new `Expr` representing the 'ends with' comparison. - */ - endsWith(suffix: string): EndsWith; - - /** - * Creates an expression that checks if a string ends with a given postfix (represented as an - * expression). - * - * ```typescript - * // Check if the 'url' field ends with the value of the 'extension' field - * Field.of("url").endsWith(Field.of("extension")); - * ``` - * - * @param suffix The postfix expression to check for. - * @return A new `Expr` representing the 'ends with' comparison. - */ - endsWith(suffix: Expr): EndsWith; - endsWith(stringOrExpr: string | Expr): EndsWith { - if (typeof stringOrExpr === 'string') { - return new EndsWith(this, Constant.of(stringOrExpr)); - } - return new EndsWith(this, stringOrExpr as Expr); - } - - /** - * Creates an expression that converts a string to lowercase. - * - * ```typescript - * // Convert the 'name' field to lowercase - * Field.of("name").toLower(); - * ``` - * - * @return A new `Expr` representing the lowercase string. - */ - toLower(): ToLower { - return new ToLower(this); - } - - /** - * Creates an expression that converts a string to uppercase. - * - * ```typescript - * // Convert the 'title' field to uppercase - * Field.of("title").toUpper(); - * ``` - * - * @return A new `Expr` representing the uppercase string. - */ - toUpper(): ToUpper { - return new ToUpper(this); - } - - /** - * Creates an expression that removes leading and trailing whitespace from a string. - * - * ```typescript - * // Trim whitespace from the 'userInput' field - * Field.of("userInput").trim(); - * ``` - * - * @return A new `Expr` representing the trimmed string. - */ - trim(): Trim { - return new Trim(this); - } - - /** - * Creates an expression that concatenates string expressions together. - * - * ```typescript - * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * Field.of("firstName").strConcat(Constant.of(" "), Field.of("lastName")); - * ``` - * - * @param elements The expressions (typically strings) to concatenate. - * @return A new `Expr` representing the concatenated string. - */ - strConcat(...elements: Array): StrConcat { - const exprs = elements.map(e => - typeof e === 'string' ? Constant.of(e) : (e as Expr) - ); - return new StrConcat(this, exprs); - } - - /** - * Creates an expression that reverses this string expression. - * - * ```typescript - * // Reverse the value of the 'myString' field. - * Field.of("myString").reverse(); - * ``` - * - * @return A new {@code Expr} representing the reversed string. - */ - reverse(): Reverse { - return new Reverse(this); - } - - /** - * Creates an expression that replaces the first occurrence of a substring within this string expression with another substring. - * - * ```typescript - * // Replace the first occurrence of "hello" with "hi" in the 'message' field - * Field.of("message").replaceFirst("hello", "hi"); - * ``` - * - * @param find The substring to search for. - * @param replace The substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. - */ - replaceFirst(find: string, replace: string): ReplaceFirst; - - /** - * Creates an expression that replaces the first occurrence of a substring within this string expression with another substring, - * where the substring to find and the replacement substring are specified by expressions. - * - * ```typescript - * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field - * Field.of("message").replaceFirst(Field.of("findField"), Field.of("replaceField")); - * ``` - * - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. - */ - replaceFirst(find: Expr, replace: Expr): ReplaceFirst; - replaceFirst(find: Expr | string, replace: Expr | string): ReplaceFirst { - const normalizedFind = typeof find === 'string' ? Constant.of(find) : find; - const normalizedReplace = - typeof replace === 'string' ? Constant.of(replace) : replace; - return new ReplaceFirst( - this, - normalizedFind as Expr, - normalizedReplace as Expr - ); - } - - /** - * Creates an expression that replaces all occurrences of a substring within this string expression with another substring. - * - * ```typescript - * // Replace all occurrences of "hello" with "hi" in the 'message' field - * Field.of("message").replaceAll("hello", "hi"); - * ``` - * - * @param find The substring to search for. - * @param replace The substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. - */ - replaceAll(find: string, replace: string): ReplaceAll; - - /** - * Creates an expression that replaces all occurrences of a substring within this string expression with another substring, - * where the substring to find and the replacement substring are specified by expressions. - * - * ```typescript - * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field - * Field.of("message").replaceAll(Field.of("findField"), Field.of("replaceField")); - * ``` - * - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. - */ - replaceAll(find: Expr, replace: Expr): ReplaceAll; - replaceAll(find: Expr | string, replace: Expr | string): ReplaceAll { - const normalizedFind = typeof find === 'string' ? Constant.of(find) : find; - const normalizedReplace = - typeof replace === 'string' ? Constant.of(replace) : replace; - return new ReplaceAll( - this, - normalizedFind as Expr, - normalizedReplace as Expr - ); - } - - /** - * Creates an expression that calculates the length of this string expression in bytes. - * - * ```typescript - * // Calculate the length of the 'myString' field in bytes. - * Field.of("myString").byteLength(); - * ``` - * - * @return A new {@code Expr} representing the length of the string in bytes. - */ - byteLength(): ByteLength { - return new ByteLength(this); - } - - /** - * Accesses a value from a map (object) field using the provided key. - * - * ```typescript - * // Get the 'city' value from the 'address' map field - * Field.of("address").mapGet("city"); - * ``` - * - * @param subfield The key to access in the map. - * @return A new `Expr` representing the value associated with the given key in the map. - */ - mapGet(subfield: string): MapGet { - return new MapGet(this, subfield); - } - - /** - * Creates an aggregation that counts the number of stage inputs with valid evaluations of the - * expression or field. - * - * ```typescript - * // Count the total number of products - * Field.of("productId").count().as("totalProducts"); - * ``` - * - * @return A new `Accumulator` representing the 'count' aggregation. - */ - count(): Count { - return new Count(this, false); - } - - /** - * Creates an aggregation that calculates the sum of a numeric field across multiple stage inputs. - * - * ```typescript - * // Calculate the total revenue from a set of orders - * Field.of("orderAmount").sum().as("totalRevenue"); - * ``` - * - * @return A new `Accumulator` representing the 'sum' aggregation. - */ - sum(): Sum { - return new Sum(this, false); - } - - /** - * Creates an aggregation that calculates the average (mean) of a numeric field across multiple - * stage inputs. - * - * ```typescript - * // Calculate the average age of users - * Field.of("age").avg().as("averageAge"); - * ``` - * - * @return A new `Accumulator` representing the 'avg' aggregation. - */ - avg(): Avg { - return new Avg(this, false); - } - - /** - * Creates an aggregation that finds the minimum value of a field across multiple stage inputs. - * - * ```typescript - * // Find the lowest price of all products - * Field.of("price").min().as("lowestPrice"); - * ``` - * - * @return A new `Accumulator` representing the 'min' aggregation. - */ - min(): Min { - return new Min(this, false); - } - - /** - * Creates an aggregation that finds the maximum value of a field across multiple stage inputs. - * - * ```typescript - * // Find the highest score in a leaderboard - * Field.of("score").max().as("highestScore"); - * ``` - * - * @return A new `Accumulator` representing the 'max' aggregation. - */ - max(): Max { - return new Max(this, false); - } - - /** - * Creates an expression that returns the larger value between this expression and another expression, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the larger value between the 'timestamp' field and the current timestamp. - * Field.of("timestamp").logicalMax(Function.currentTimestamp()); - * ``` - * - * @param other The expression to compare with. - * @return A new {@code Expr} representing the logical max operation. - */ - logicalMax(other: Expr): LogicalMax; - - /** - * Creates an expression that returns the larger value between this expression and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the larger value between the 'value' field and 10. - * Field.of("value").logicalMax(10); - * ``` - * - * @param other The constant value to compare with. - * @return A new {@code Expr} representing the logical max operation. - */ - logicalMax(other: any): LogicalMax; - logicalMax(other: any): LogicalMax { - if (other instanceof Expr) { - return new LogicalMax(this, other as Expr); - } - return new LogicalMax(this, Constant.of(other)); - } - - /** - * Creates an expression that returns the smaller value between this expression and another expression, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the smaller value between the 'timestamp' field and the current timestamp. - * Field.of("timestamp").logicalMin(Function.currentTimestamp()); - * ``` - * - * @param other The expression to compare with. - * @return A new {@code Expr} representing the logical min operation. - */ - logicalMin(other: Expr): LogicalMin; - - /** - * Creates an expression that returns the smaller value between this expression and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the smaller value between the 'value' field and 10. - * Field.of("value").logicalMin(10); - * ``` - * - * @param other The constant value to compare with. - * @return A new {@code Expr} representing the logical min operation. - */ - logicalMin(other: any): LogicalMin; - logicalMin(other: any): LogicalMin { - if (other instanceof Expr) { - return new LogicalMin(this, other as Expr); - } - return new LogicalMin(this, Constant.of(other)); - } - - /** - * Creates an expression that calculates the length (number of dimensions) of this Firestore Vector expression. - * - * ```typescript - * // Get the vector length (dimension) of the field 'embedding'. - * Field.of("embedding").vectorLength(); - * ``` - * - * @return A new {@code Expr} representing the length of the vector. - */ - vectorLength(): VectorLength { - return new VectorLength(this); - } - - /** - * Calculates the cosine distance between two vectors. - * - * ```typescript - * // Calculate the cosine distance between the 'userVector' field and the 'itemVector' field - * Field.of("userVector").cosineDistance(Field.of("itemVector")); - * ``` - * - * @param other The other vector (represented as an Expr) to compare against. - * @return A new `Expr` representing the cosine distance between the two vectors. - */ - cosineDistance(other: Expr): CosineDistance; - /** - * Calculates the Cosine distance between two vectors. - * - * ```typescript - * // Calculate the Cosine distance between the 'location' field and a target location - * Field.of("location").cosineDistance(new VectorValue([37.7749, -122.4194])); - * ``` - * - * @param other The other vector (as a VectorValue) to compare against. - * @return A new `Expr` representing the Cosine* distance between the two vectors. - */ - cosineDistance(other: VectorValue): CosineDistance; - /** - * Calculates the Cosine distance between two vectors. - * - * ```typescript - * // Calculate the Cosine distance between the 'location' field and a target location - * Field.of("location").cosineDistance([37.7749, -122.4194]); - * ``` - * - * @param other The other vector (as an array of numbers) to compare against. - * @return A new `Expr` representing the Cosine distance between the two vectors. - */ - cosineDistance(other: number[]): CosineDistance; - cosineDistance(other: Expr | VectorValue | number[]): CosineDistance { - if (other instanceof Expr) { - return new CosineDistance(this, other as Expr); - } else { - return new CosineDistance( - this, - Constant.vector(other as VectorValue | number[]) - ); - } - } - - /** - * Calculates the dot product between two vectors. - * - * ```typescript - * // Calculate the dot product between a feature vector and a target vector - * Field.of("features").dotProduct([0.5, 0.8, 0.2]); - * ``` - * - * @param other The other vector (as an array of numbers) to calculate with. - * @return A new `Expr` representing the dot product between the two vectors. - */ - dotProduct(other: Expr): DotProduct; - - /** - * Calculates the dot product between two vectors. - * - * ```typescript - * // Calculate the dot product between a feature vector and a target vector - * Field.of("features").dotProduct(new VectorValue([0.5, 0.8, 0.2])); - * ``` - * - * @param other The other vector (as an array of numbers) to calculate with. - * @return A new `Expr` representing the dot product between the two vectors. - */ - dotProduct(other: VectorValue): DotProduct; - - /** - * Calculates the dot product between two vectors. - * - * ```typescript - * // Calculate the dot product between a feature vector and a target vector - * Field.of("features").dotProduct([0.5, 0.8, 0.2]); - * ``` - * - * @param other The other vector (as an array of numbers) to calculate with. - * @return A new `Expr` representing the dot product between the two vectors. - */ - dotProduct(other: number[]): DotProduct; - dotProduct(other: Expr | VectorValue | number[]): DotProduct { - if (other instanceof Expr) { - return new DotProduct(this, other as Expr); - } else { - return new DotProduct( - this, - Constant.vector(other as VectorValue | number[]) - ); - } - } - - /** - * Calculates the Euclidean distance between two vectors. - * - * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * Field.of("location").euclideanDistance([37.7749, -122.4194]); - * ``` - * - * @param other The other vector (as an array of numbers) to calculate with. - * @return A new `Expr` representing the Euclidean distance between the two vectors. - */ - euclideanDistance(other: Expr): EuclideanDistance; - - /** - * Calculates the Euclidean distance between two vectors. - * - * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * Field.of("location").euclideanDistance(new VectorValue([37.7749, -122.4194])); - * ``` - * - * @param other The other vector (as a VectorValue) to compare against. - * @return A new `Expr` representing the Euclidean distance between the two vectors. - */ - euclideanDistance(other: VectorValue): EuclideanDistance; - - /** - * Calculates the Euclidean distance between two vectors. - * - * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * Field.of("location").euclideanDistance([37.7749, -122.4194]); - * ``` - * - * @param other The other vector (as an array of numbers) to compare against. - * @return A new `Expr` representing the Euclidean distance between the two vectors. - */ - euclideanDistance(other: number[]): EuclideanDistance; - euclideanDistance(other: Expr | VectorValue | number[]): EuclideanDistance { - if (other instanceof Expr) { - return new EuclideanDistance(this, other as Expr); - } else { - return new EuclideanDistance( - this, - Constant.vector(other as VectorValue | number[]) - ); - } - } - - /** - * Creates an expression that interprets this expression as the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC) - * and returns a timestamp. - * - * ```typescript - * // Interpret the 'microseconds' field as microseconds since epoch. - * Field.of("microseconds").unixMicrosToTimestamp(); - * ``` - * - * @return A new {@code Expr} representing the timestamp. - */ - unixMicrosToTimestamp(): UnixMicrosToTimestamp { - return new UnixMicrosToTimestamp(this); - } - - /** - * Creates an expression that converts this timestamp expression to the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC). - * - * ```typescript - * // Convert the 'timestamp' field to microseconds since epoch. - * Field.of("timestamp").timestampToUnixMicros(); - * ``` - * - * @return A new {@code Expr} representing the number of microseconds since epoch. - */ - timestampToUnixMicros(): TimestampToUnixMicros { - return new TimestampToUnixMicros(this); - } - - /** - * Creates an expression that interprets this expression as the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC) - * and returns a timestamp. - * - * ```typescript - * // Interpret the 'milliseconds' field as milliseconds since epoch. - * Field.of("milliseconds").unixMillisToTimestamp(); - * ``` - * - * @return A new {@code Expr} representing the timestamp. - */ - unixMillisToTimestamp(): UnixMillisToTimestamp { - return new UnixMillisToTimestamp(this); - } - - /** - * Creates an expression that converts this timestamp expression to the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC). - * - * ```typescript - * // Convert the 'timestamp' field to milliseconds since epoch. - * Field.of("timestamp").timestampToUnixMillis(); - * ``` - * - * @return A new {@code Expr} representing the number of milliseconds since epoch. - */ - timestampToUnixMillis(): TimestampToUnixMillis { - return new TimestampToUnixMillis(this); - } - - /** - * Creates an expression that interprets this expression as the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC) - * and returns a timestamp. - * - * ```typescript - * // Interpret the 'seconds' field as seconds since epoch. - * Field.of("seconds").unixSecondsToTimestamp(); - * ``` - * - * @return A new {@code Expr} representing the timestamp. - */ - unixSecondsToTimestamp(): UnixSecondsToTimestamp { - return new UnixSecondsToTimestamp(this); - } - - /** - * Creates an expression that converts this timestamp expression to the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC). - * - * ```typescript - * // Convert the 'timestamp' field to seconds since epoch. - * Field.of("timestamp").timestampToUnixSeconds(); - * ``` - * - * @return A new {@code Expr} representing the number of seconds since epoch. - */ - timestampToUnixSeconds(): TimestampToUnixSeconds { - return new TimestampToUnixSeconds(this); - } - - /** - * Creates an expression that adds a specified amount of time to this timestamp expression. - * - * ```typescript - * // Add some duration determined by field 'unit' and 'amount' to the 'timestamp' field. - * Field.of("timestamp").timestampAdd(Field.of("unit"), Field.of("amount")); - * ``` - * - * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. - * @param amount The expression evaluates to amount of the unit. - * @return A new {@code Expr} representing the resulting timestamp. - */ - timestampAdd(unit: Expr, amount: Expr): TimestampAdd; - - /** - * Creates an expression that adds a specified amount of time to this timestamp expression. - * - * ```typescript - * // Add 1 day to the 'timestamp' field. - * Field.of("timestamp").timestampAdd("day", 1); - * ``` - * - * @param unit The unit of time to add (e.g., "day", "hour"). - * @param amount The amount of time to add. - * @return A new {@code Expr} representing the resulting timestamp. - */ - timestampAdd( - unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: number - ): TimestampAdd; - timestampAdd( - unit: - | Expr - | 'microsecond' - | 'millisecond' - | 'second' - | 'minute' - | 'hour' - | 'day', - amount: Expr | number - ): TimestampAdd { - const normalizedUnit = typeof unit === 'string' ? Constant.of(unit) : unit; - const normalizedAmount = - typeof amount === 'number' ? Constant.of(amount) : amount; - return new TimestampAdd( - this, - normalizedUnit as Expr, - normalizedAmount as Expr - ); - } - - /** - * Creates an expression that subtracts a specified amount of time from this timestamp expression. - * - * ```typescript - * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. - * Field.of("timestamp").timestampSub(Field.of("unit"), Field.of("amount")); - * ``` - * - * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. - * @param amount The expression evaluates to amount of the unit. - * @return A new {@code Expr} representing the resulting timestamp. - */ - timestampSub(unit: Expr, amount: Expr): TimestampSub; - - /** - * Creates an expression that subtracts a specified amount of time from this timestamp expression. - * - * ```typescript - * // Subtract 1 day from the 'timestamp' field. - * Field.of("timestamp").timestampSub("day", 1); - * ``` - * - * @param unit The unit of time to subtract (e.g., "day", "hour"). - * @param amount The amount of time to subtract. - * @return A new {@code Expr} representing the resulting timestamp. - */ - timestampSub( - unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: number - ): TimestampSub; - timestampSub( - unit: - | Expr - | 'microsecond' - | 'millisecond' - | 'second' - | 'minute' - | 'hour' - | 'day', - amount: Expr | number - ): TimestampSub { - const normalizedUnit = typeof unit === 'string' ? Constant.of(unit) : unit; - const normalizedAmount = - typeof amount === 'number' ? Constant.of(amount) : amount; - return new TimestampSub( - this, - normalizedUnit as Expr, - normalizedAmount as Expr - ); - } - - /** - * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. - * - * ```typescript - * // Sort documents by the 'name' field in ascending order - * pipeline().collection("users") - * .sort(Field.of("name").ascending()); - * ``` - * - * @return A new `Ordering` for ascending sorting. - */ - ascending(): Ordering { - return ascending(this); - } - - /** - * Creates an {@link Ordering} that sorts documents in descending order based on this expression. - * - * ```typescript - * // Sort documents by the 'createdAt' field in descending order - * firestore.pipeline().collection("users") - * .sort(Field.of("createdAt").descending()); - * ``` - * - * @return A new `Ordering` for descending sorting. - */ - descending(): Ordering { - return descending(this); - } - - /** - * Assigns an alias to this expression. - * - * Aliases are useful for renaming fields in the output of a stage or for giving meaningful - * names to calculated values. - * - * ```typescript - * // Calculate the total price and assign it the alias "totalPrice" and add it to the output. - * firestore.pipeline().collection("items") - * .addFields(Field.of("price").multiply(Field.of("quantity")).as("totalPrice")); - * ``` - * - * @param name The alias to assign to this expression. - * @return A new {@link ExprWithAlias} that wraps this - * expression and associates it with the provided alias. - */ - as(name: string): ExprWithAlias { - return new ExprWithAlias(this, name); - } - - /** - * @private - * @internal - */ - abstract _toProto(serializer: JsonProtoSerializer): ProtoValue; - - /** - * @private - * @internal - */ - abstract _readUserData(dataReader: UserDataReader): void; -} - -/** - * @beta - */ -export class ExprWithAlias extends Expr implements Selectable { - exprType: ExprType = 'ExprWithAlias'; - selectable = true as const; - - constructor(public expr: T, public alias: string) { - super(); - } - - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { - throw new Error('ExprWithAlias should not be serialized directly.'); - } - - /** - * @private - * @internal - */ - _readUserData(dataReader: UserDataReader): void { - this.expr._readUserData(dataReader); - } -} - -/** - * @internal - */ -class ListOfExprs extends Expr { - exprType: ExprType = 'ListOfExprs'; - constructor(private exprs: Expr[]) { - super(); - } - - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { - return { - arrayValue: { - values: this.exprs.map(p => p._toProto(serializer)!) - } - }; - } - - /** - * @private - * @internal - */ - _readUserData(dataReader: UserDataReader): void { - this.exprs.forEach((expr: Expr) => expr._readUserData(dataReader)); - } -} - -/** - * @beta - * - * Represents a reference to a field in a Firestore document, or outputs of a {@link Pipeline} stage. - * - *

    Field references are used to access document field values in expressions and to specify fields - * for sorting, filtering, and projecting data in Firestore pipelines. - * - *

    You can create a `Field` instance using the static {@link #of} method: - * - * ```typescript - * // Create a Field instance for the 'name' field - * const nameField = Field.of("name"); - * - * // Create a Field instance for a nested field 'address.city' - * const cityField = Field.of("address.city"); - * ``` - */ -export class Field extends Expr implements Selectable { - exprType: ExprType = 'Field'; - selectable = true as const; - - private constructor( - private fieldPath: InternalFieldPath, - private pipeline: Pipeline | null = null - ) { - super(); - } - - /** - * Creates a {@code Field} instance representing the field at the given path. - * - * The path can be a simple field name (e.g., "name") or a dot-separated path to a nested field - * (e.g., "address.city"). - * - * ```typescript - * // Create a Field instance for the 'title' field - * const titleField = Field.of("title"); - * - * // Create a Field instance for a nested field 'author.firstName' - * const authorFirstNameField = Field.of("author.firstName"); - * ``` - * - * @param name The path to the field. - * @return A new {@code Field} instance representing the specified field. - */ - static of(name: string): Field; - static of(path: FieldPath): Field; - static of(pipeline: Pipeline, name: string): Field; - static of( - pipelineOrName: Pipeline | string | FieldPath, - name?: string - ): Field { - if (typeof pipelineOrName === 'string') { - if (DOCUMENT_KEY_NAME === pipelineOrName) { - return new Field(documentId()._internalPath); - } - return new Field(fieldPathFromArgument('of', pipelineOrName)); - } else if (pipelineOrName instanceof FieldPath) { - if (documentId().isEqual(pipelineOrName)) { - return new Field(documentId()._internalPath); - } - return new Field(pipelineOrName._internalPath); - } else { - return new Field( - fieldPathFromArgument('of', name!), - pipelineOrName as Pipeline - ); - } - } - - fieldName(): string { - return this.fieldPath.canonicalString(); - } - - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { - return { - fieldReferenceValue: this.fieldPath.canonicalString() - }; - } - - /** - * @private - * @internal - */ - _readUserData(dataReader: UserDataReader): void {} -} - -/** - * @beta - */ -export class Fields extends Expr implements Selectable { - exprType: ExprType = 'Field'; - selectable = true as const; - - private constructor(private fields: Field[]) { - super(); - } - - static of(name: string, ...others: string[]): Fields { - return new Fields([Field.of(name), ...others.map(Field.of)]); - } - - static ofAll(): Fields { - return new Fields([]); - } - - fieldList(): Field[] { - return this.fields.map(f => f); - } - - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { - return { - arrayValue: { - values: this.fields.map(f => f._toProto(serializer)) - } - }; - } - - /** - * @private - * @internal - */ - _readUserData(dataReader: UserDataReader): void { - this.fields.forEach(expr => expr._readUserData(dataReader)); - } -} - -/** - * @beta - * - * Represents a constant value that can be used in a Firestore pipeline expression. - * - * You can create a `Constant` instance using the static {@link #of} method: - * - * ```typescript - * // Create a Constant instance for the number 10 - * const ten = Constant.of(10); - * - * // Create a Constant instance for the string "hello" - * const hello = Constant.of("hello"); - * ``` - */ -export class Constant extends Expr { - exprType: ExprType = 'Constant'; - - private _protoValue?: ProtoValue; - - private constructor(private value: any) { - super(); - } - - /** - * Creates a `Constant` instance for a number value. - * - * @param value The number value. - * @return A new `Constant` instance. - */ - static of(value: number): Constant; - - /** - * Creates a `Constant` instance for a string value. - * - * @param value The string value. - * @return A new `Constant` instance. - */ - static of(value: string): Constant; - - /** - * Creates a `Constant` instance for a boolean value. - * - * @param value The boolean value. - * @return A new `Constant` instance. - */ - static of(value: boolean): Constant; - - /** - * Creates a `Constant` instance for a null value. - * - * @param value The null value. - * @return A new `Constant` instance. - */ - static of(value: null): Constant; - - /** - * Creates a `Constant` instance for an undefined value. - * - * @param value The undefined value. - * @return A new `Constant` instance. - */ - static of(value: undefined): Constant; - - /** - * Creates a `Constant` instance for a GeoPoint value. - * - * @param value The GeoPoint value. - * @return A new `Constant` instance. - */ - static of(value: GeoPoint): Constant; - - /** - * Creates a `Constant` instance for a Timestamp value. - * - * @param value The Timestamp value. - * @return A new `Constant` instance. - */ - static of(value: Timestamp): Constant; - - /** - * Creates a `Constant` instance for a Date value. - * - * @param value The Date value. - * @return A new `Constant` instance. - */ - static of(value: Date): Constant; - - /** - * Creates a `Constant` instance for a Uint8Array value. - * - * @param value The Uint8Array value. - * @return A new `Constant` instance. - */ - static of(value: Uint8Array): Constant; - - /** - * Creates a `Constant` instance for a DocumentReference value. - * - * @param value The DocumentReference value. - * @return A new `Constant` instance. - */ - static of(value: DocumentReference): Constant; - - // TODO(pipeline) if we make this public, then the Proto types should also be documented - /** - * Creates a `Constant` instance for a Firestore proto value. - * @private - * @internal - * @param value The Firestore proto value. - * @return A new `Constant` instance. - */ - static of(value: ProtoValue): Constant; - - /** - * Creates a `Constant` instance for an array value. - * - * @param value The array value. - * @return A new `Constant` instance. - */ - static of(value: any[]): Constant; - - /** - * Creates a `Constant` instance for a map value. - * - * @param value The map value. - * @return A new `Constant` instance. - */ - static of(value: Map): Constant; - - /** - * Creates a `Constant` instance for a VectorValue value. - * - * @param value The VectorValue value. - * @return A new `Constant` instance. - */ - static of(value: VectorValue): Constant; - - static of(value: any): Constant { - return new Constant(value); - } - - /** - * Creates a `Constant` instance for a VectorValue value. - * - * ```typescript - * // Create a Constant instance for a vector value - * const vectorConstant = Constant.ofVector([1, 2, 3]); - * ``` - * - * @param value The VectorValue value. - * @return A new `Constant` instance. - */ - static vector(value: number[] | VectorValue): Constant { - if (value instanceof VectorValue) { - return new Constant(value); - } else { - return new Constant(new VectorValue(value as number[])); - } - } - - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { - hardAssert( - this._protoValue !== undefined, - 0x3e26, - 'Value of this constant has not been serialized to proto value' - ); - return this._protoValue; - } - - /** - * @private - * @internal - */ - _readUserData(dataReader: UserDataReader): void { - const context = dataReader.createContext( - UserDataSource.Argument, - 'Constant.of' - ); - if (this.value === undefined) { - // TODO how should we treat the value of `undefined`? - this._protoValue = parseData(null, context)!; - } else { - this._protoValue = parseData(this.value, context)!; - } - } -} - -/** - * @beta - * - * This class defines the base class for Firestore {@link Pipeline} functions, which can be evaluated within pipeline - * execution. - * - * Typically, you would not use this class or its children directly. Use either the functions like {@link and}, {@link eq}, - * or the methods on {@link Expr} ({@link Expr#eq}, {@link Expr#lt}, etc) to construct new Function instances. - */ -export class FirestoreFunction extends Expr { - exprType: ExprType = 'Function'; - constructor(private name: string, private params: Expr[]) { - super(); - } - - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { - return { - functionValue: { - name: this.name, - args: this.params.map(p => p._toProto(serializer)) - } - }; - } - - /** - * @private - * @internal - */ - _readUserData(dataReader: UserDataReader): void { - this.params.forEach(expr => expr._readUserData(dataReader)); - } -} - -/** - * @beta - */ -export class Add extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('add', [left, right]); - } -} - -/** - * @beta - */ -export class Subtract extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('subtract', [left, right]); - } -} - -/** - * @beta - */ -export class Multiply extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('multiply', [left, right]); - } -} - -/** - * @beta - */ -export class Divide extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('divide', [left, right]); - } -} - -/** - * @beta - */ -export class Mod extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('mod', [left, right]); - } -} - -// /** -// * @beta -// */ -// export class BitAnd extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_and', [left, right]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitOr extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_or', [left, right]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitXor extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_xor', [left, right]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitNot extends FirestoreFunction { -// constructor(private operand: Expr) { -// super('bit_not', [operand]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitLeftShift extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_left_shift', [left, right]); -// } -// } -// -// /** -// * @beta -// */ -// export class BitRightShift extends FirestoreFunction { -// constructor( -// private left: Expr, -// private right: Expr -// ) { -// super('bit_right_shift', [left, right]); -// } -// } - -/** - * @beta - */ -export class Eq extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('eq', [left, right]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class Neq extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('neq', [left, right]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class Lt extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('lt', [left, right]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class Lte extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('lte', [left, right]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class Gt extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('gt', [left, right]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class Gte extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private right: Expr) { - super('gte', [left, right]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class ArrayConcat extends FirestoreFunction { - constructor(private array: Expr, private elements: Expr[]) { - super('array_concat', [array, ...elements]); - } -} - -/** - * @beta - */ -export class ArrayReverse extends FirestoreFunction { - constructor(private array: Expr) { - super('array_reverse', [array]); - } -} - -/** - * @beta - */ -export class ArrayContains - extends FirestoreFunction - implements FilterCondition -{ - constructor(private array: Expr, private element: Expr) { - super('array_contains', [array, element]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class ArrayContainsAll - extends FirestoreFunction - implements FilterCondition -{ - constructor(private array: Expr, private values: Expr[]) { - super('array_contains_all', [array, new ListOfExprs(values)]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class ArrayContainsAny - extends FirestoreFunction - implements FilterCondition -{ - constructor(private array: Expr, private values: Expr[]) { - super('array_contains_any', [array, new ListOfExprs(values)]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class ArrayLength extends FirestoreFunction { - constructor(private array: Expr) { - super('array_length', [array]); - } -} - -/** - * @beta - */ -export class ArrayElement extends FirestoreFunction { - constructor() { - super('array_element', []); - } -} - -/** - * @beta - */ -export class In extends FirestoreFunction implements FilterCondition { - constructor(private left: Expr, private others: Expr[]) { - super('in', [left, new ListOfExprs(others)]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class IsNan extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr) { - super('is_nan', [expr]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class Exists extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr) { - super('exists', [expr]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class Not extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr) { - super('not', [expr]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class And extends FirestoreFunction implements FilterCondition { - constructor(private conditions: FilterExpr[]) { - super('and', conditions); - } - - filterable = true as const; -} - -/** - * @beta - */ -export class Or extends FirestoreFunction implements FilterCondition { - constructor(private conditions: FilterExpr[]) { - super('or', conditions); - } - filterable = true as const; -} - -/** - * @beta - */ -export class Xor extends FirestoreFunction implements FilterCondition { - constructor(private conditions: FilterExpr[]) { - super('xor', conditions); - } - filterable = true as const; -} - -/** - * @beta - */ -export class If extends FirestoreFunction implements FilterCondition { - constructor( - private condition: FilterExpr, - private thenExpr: Expr, - private elseExpr: Expr - ) { - super('if', [condition, thenExpr, elseExpr]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class LogicalMax extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('logical_max', [left, right]); - } -} - -/** - * @beta - */ -export class LogicalMin extends FirestoreFunction { - constructor(private left: Expr, private right: Expr) { - super('logical_min', [left, right]); - } -} - -/** - * @beta - */ -export class Reverse extends FirestoreFunction { - constructor(private value: Expr) { - super('reverse', [value]); - } -} - -/** - * @beta - */ -export class ReplaceFirst extends FirestoreFunction { - constructor(private value: Expr, private find: Expr, private replace: Expr) { - super('replace_first', [value, find, replace]); - } -} - -/** - * @beta - */ -export class ReplaceAll extends FirestoreFunction { - constructor(private value: Expr, private find: Expr, private replace: Expr) { - super('replace_all', [value, find, replace]); - } -} - -/** - * @beta - */ -export class CharLength extends FirestoreFunction { - constructor(private value: Expr) { - super('char_length', [value]); - } -} - -/** - * @beta - */ -export class ByteLength extends FirestoreFunction { - constructor(private value: Expr) { - super('byte_length', [value]); - } -} - -/** - * @beta - */ -export class Like extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private pattern: Expr) { - super('like', [expr, pattern]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class RegexContains - extends FirestoreFunction - implements FilterCondition -{ - constructor(private expr: Expr, private pattern: Expr) { - super('regex_contains', [expr, pattern]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class RegexMatch extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private pattern: Expr) { - super('regex_match', [expr, pattern]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class StrContains extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private substring: Expr) { - super('str_contains', [expr, substring]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class StartsWith extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private prefix: Expr) { - super('starts_with', [expr, prefix]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class EndsWith extends FirestoreFunction implements FilterCondition { - constructor(private expr: Expr, private suffix: Expr) { - super('ends_with', [expr, suffix]); - } - filterable = true as const; -} - -/** - * @beta - */ -export class ToLower extends FirestoreFunction { - constructor(private expr: Expr) { - super('to_lower', [expr]); - } -} - -/** - * @beta - */ -export class ToUpper extends FirestoreFunction { - constructor(private expr: Expr) { - super('to_upper', [expr]); - } -} - -/** - * @beta - */ -export class Trim extends FirestoreFunction { - constructor(private expr: Expr) { - super('trim', [expr]); - } -} - -/** - * @beta - */ -export class StrConcat extends FirestoreFunction { - constructor(private first: Expr, private rest: Expr[]) { - super('str_concat', [first, ...rest]); - } -} - -/** - * @beta - */ -export class MapGet extends FirestoreFunction { - constructor(map: Expr, name: string) { - super('map_get', [map, Constant.of(name)]); - } -} - -/** - * @beta - */ -export class Count extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr | undefined, private distinct: boolean) { - super('count', value === undefined ? [] : [value]); - } -} - -/** - * @beta - */ -export class Sum extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr, private distinct: boolean) { - super('sum', [value]); - } -} - -/** - * @beta - */ -export class Avg extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr, private distinct: boolean) { - super('avg', [value]); - } -} - -/** - * @beta - */ -export class Min extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr, private distinct: boolean) { - super('min', [value]); - } -} - -/** - * @beta - */ -export class Max extends FirestoreFunction implements Accumulator { - accumulator = true as const; - constructor(private value: Expr, private distinct: boolean) { - super('max', [value]); - } -} - -/** - * @beta - */ -export class CosineDistance extends FirestoreFunction { - constructor(private vector1: Expr, private vector2: Expr) { - super('cosine_distance', [vector1, vector2]); - } -} - -/** - * @beta - */ -export class DotProduct extends FirestoreFunction { - constructor(private vector1: Expr, private vector2: Expr) { - super('dot_product', [vector1, vector2]); - } -} - -/** - * @beta - */ -export class EuclideanDistance extends FirestoreFunction { - constructor(private vector1: Expr, private vector2: Expr) { - super('euclidean_distance', [vector1, vector2]); - } -} - -/** - * @beta - */ -export class VectorLength extends FirestoreFunction { - constructor(private value: Expr) { - super('vector_length', [value]); - } -} - -/** - * @beta - */ -export class UnixMicrosToTimestamp extends FirestoreFunction { - constructor(private input: Expr) { - super('unix_micros_to_timestamp', [input]); - } -} - -/** - * @beta - */ -export class TimestampToUnixMicros extends FirestoreFunction { - constructor(private input: Expr) { - super('timestamp_to_unix_micros', [input]); - } -} - -/** - * @beta - */ -export class UnixMillisToTimestamp extends FirestoreFunction { - constructor(private input: Expr) { - super('unix_millis_to_timestamp', [input]); - } -} - -/** - * @beta - */ -export class TimestampToUnixMillis extends FirestoreFunction { - constructor(private input: Expr) { - super('timestamp_to_unix_millis', [input]); - } -} - -/** - * @beta - */ -export class UnixSecondsToTimestamp extends FirestoreFunction { - constructor(private input: Expr) { - super('unix_seconds_to_timestamp', [input]); - } -} - -/** - * @beta - */ -export class TimestampToUnixSeconds extends FirestoreFunction { - constructor(private input: Expr) { - super('timestamp_to_unix_seconds', [input]); - } -} - -/** - * @beta - */ -export class TimestampAdd extends FirestoreFunction { - constructor( - private timestamp: Expr, - private unit: Expr, - private amount: Expr - ) { - super('timestamp_add', [timestamp, unit, amount]); - } -} - -/** - * @beta - */ -export class TimestampSub extends FirestoreFunction { - constructor( - private timestamp: Expr, - private unit: Expr, - private amount: Expr - ) { - super('timestamp_sub', [timestamp, unit, amount]); - } -} - -/** - * @beta - * - * Creates an expression that adds two expressions together. - * - * ```typescript - * // Add the value of the 'quantity' field and the 'reserve' field. - * add(Field.of("quantity"), Field.of("reserve")); - * ``` - * - * @param left The first expression to add. - * @param right The second expression to add. - * @return A new {@code Expr} representing the addition operation. - */ -export function add(left: Expr, right: Expr): Add; - -/** - * @beta - * - * Creates an expression that adds an expression to a constant value. - * - * ```typescript - * // Add 5 to the value of the 'age' field - * add(Field.of("age"), 5); - * ``` - * - * @param left The expression to add to. - * @param right The constant value to add. - * @return A new {@code Expr} representing the addition operation. - */ -export function add(left: Expr, right: any): Add; - -/** - * @beta - * - * Creates an expression that adds a field's value to an expression. - * - * ```typescript - * // Add the value of the 'quantity' field and the 'reserve' field. - * add("quantity", Field.of("reserve")); - * ``` - * - * @param left The field name to add to. - * @param right The expression to add. - * @return A new {@code Expr} representing the addition operation. - */ -export function add(left: string, right: Expr): Add; - -/** - * @beta - * - * Creates an expression that adds a field's value to a constant value. - * - * ```typescript - * // Add 5 to the value of the 'age' field - * add("age", 5); - * ``` - * - * @param left The field name to add to. - * @param right The constant value to add. - * @return A new {@code Expr} representing the addition operation. - */ -export function add(left: string, right: any): Add; -export function add(left: Expr | string, right: Expr | any): Add { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Add(normalizedLeft, normalizedRight); -} - -/** - * @beta - * - * Creates an expression that subtracts two expressions. - * - * ```typescript - * // Subtract the 'discount' field from the 'price' field - * subtract(Field.of("price"), Field.of("discount")); - * ``` - * - * @param left The expression to subtract from. - * @param right The expression to subtract. - * @return A new {@code Expr} representing the subtraction operation. - */ -export function subtract(left: Expr, right: Expr): Subtract; - -/** - * @beta - * - * Creates an expression that subtracts a constant value from an expression. - * - * ```typescript - * // Subtract the constant value 2 from the 'value' field - * subtract(Field.of("value"), 2); - * ``` - * - * @param left The expression to subtract from. - * @param right The constant value to subtract. - * @return A new {@code Expr} representing the subtraction operation. - */ -export function subtract(left: Expr, right: any): Subtract; - -/** - * @beta - * - * Creates an expression that subtracts an expression from a field's value. - * - * ```typescript - * // Subtract the 'discount' field from the 'price' field - * subtract("price", Field.of("discount")); - * ``` - * - * @param left The field name to subtract from. - * @param right The expression to subtract. - * @return A new {@code Expr} representing the subtraction operation. - */ -export function subtract(left: string, right: Expr): Subtract; - -/** - * @beta - * - * Creates an expression that subtracts a constant value from a field's value. - * - * ```typescript - * // Subtract 20 from the value of the 'total' field - * subtract("total", 20); - * ``` - * - * @param left The field name to subtract from. - * @param right The constant value to subtract. - * @return A new {@code Expr} representing the subtraction operation. - */ -export function subtract(left: string, right: any): Subtract; -export function subtract(left: Expr | string, right: Expr | any): Subtract { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Subtract(normalizedLeft, normalizedRight); -} - -/** - * @beta - * - * Creates an expression that multiplies two expressions together. - * - * ```typescript - * // Multiply the 'quantity' field by the 'price' field - * multiply(Field.of("quantity"), Field.of("price")); - * ``` - * - * @param left The first expression to multiply. - * @param right The second expression to multiply. - * @return A new {@code Expr} representing the multiplication operation. - */ -export function multiply(left: Expr, right: Expr): Multiply; - -/** - * @beta - * - * Creates an expression that multiplies an expression by a constant value. - * - * ```typescript - * // Multiply the value of the 'price' field by 2 - * multiply(Field.of("price"), 2); - * ``` - * - * @param left The expression to multiply. - * @param right The constant value to multiply by. - * @return A new {@code Expr} representing the multiplication operation. - */ -export function multiply(left: Expr, right: any): Multiply; - -/** - * @beta - * - * Creates an expression that multiplies a field's value by an expression. - * - * ```typescript - * // Multiply the 'quantity' field by the 'price' field - * multiply("quantity", Field.of("price")); - * ``` - * - * @param left The field name to multiply. - * @param right The expression to multiply by. - * @return A new {@code Expr} representing the multiplication operation. - */ -export function multiply(left: string, right: Expr): Multiply; - -/** - * @beta - * - * Creates an expression that multiplies a field's value by a constant value. - * - * ```typescript - * // Multiply the 'value' field by 2 - * multiply("value", 2); - * ``` - * - * @param left The field name to multiply. - * @param right The constant value to multiply by. - * @return A new {@code Expr} representing the multiplication operation. - */ -export function multiply(left: string, right: any): Multiply; -export function multiply(left: Expr | string, right: Expr | any): Multiply { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Multiply(normalizedLeft, normalizedRight); -} - -/** - * @beta - * - * Creates an expression that divides two expressions. - * - * ```typescript - * // Divide the 'total' field by the 'count' field - * divide(Field.of("total"), Field.of("count")); - * ``` - * - * @param left The expression to be divided. - * @param right The expression to divide by. - * @return A new {@code Expr} representing the division operation. - */ -export function divide(left: Expr, right: Expr): Divide; - -/** - * @beta - * - * Creates an expression that divides an expression by a constant value. - * - * ```typescript - * // Divide the 'value' field by 10 - * divide(Field.of("value"), 10); - * ``` - * - * @param left The expression to be divided. - * @param right The constant value to divide by. - * @return A new {@code Expr} representing the division operation. - */ -export function divide(left: Expr, right: any): Divide; - -/** - * @beta - * - * Creates an expression that divides a field's value by an expression. - * - * ```typescript - * // Divide the 'total' field by the 'count' field - * divide("total", Field.of("count")); - * ``` - * - * @param left The field name to be divided. - * @param right The expression to divide by. - * @return A new {@code Expr} representing the division operation. - */ -export function divide(left: string, right: Expr): Divide; - -/** - * @beta - * - * Creates an expression that divides a field's value by a constant value. - * - * ```typescript - * // Divide the 'value' field by 10 - * divide("value", 10); - * ``` - * - * @param left The field name to be divided. - * @param right The constant value to divide by. - * @return A new {@code Expr} representing the division operation. - */ -export function divide(left: string, right: any): Divide; -export function divide(left: Expr | string, right: Expr | any): Divide { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Divide(normalizedLeft, normalizedRight); -} - -/** - * @beta - * - * Creates an expression that calculates the modulo (remainder) of dividing two expressions. - * - * ```typescript - * // Calculate the remainder of dividing 'field1' by 'field2'. - * mod(Field.of("field1"), Field.of("field2")); - * ``` - * - * @param left The dividend expression. - * @param right The divisor expression. - * @return A new {@code Expr} representing the modulo operation. - */ -export function mod(left: Expr, right: Expr): Mod; - -/** - * @beta - * - * Creates an expression that calculates the modulo (remainder) of dividing an expression by a constant. - * - * ```typescript - * // Calculate the remainder of dividing 'field1' by 5. - * mod(Field.of("field1"), 5); - * ``` - * - * @param left The dividend expression. - * @param right The divisor constant. - * @return A new {@code Expr} representing the modulo operation. - */ -export function mod(left: Expr, right: any): Mod; - -/** - * @beta - * - * Creates an expression that calculates the modulo (remainder) of dividing a field's value by an expression. - * - * ```typescript - * // Calculate the remainder of dividing 'field1' by 'field2'. - * mod("field1", Field.of("field2")); - * ``` - * - * @param left The dividend field name. - * @param right The divisor expression. - * @return A new {@code Expr} representing the modulo operation. - */ -export function mod(left: string, right: Expr): Mod; - -/** - * @beta - * - * Creates an expression that calculates the modulo (remainder) of dividing a field's value by a constant. - * - * ```typescript - * // Calculate the remainder of dividing 'field1' by 5. - * mod("field1", 5); - * ``` - * - * @param left The dividend field name. - * @param right The divisor constant. - * @return A new {@code Expr} representing the modulo operation. - */ -export function mod(left: string, right: any): Mod; -export function mod(left: Expr | string, right: Expr | any): Mod { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new Mod(normalizedLeft, normalizedRight); -} - -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise AND operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise AND of 'field1' and 'field2'. -// * bitAnd(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise AND operation. -// */ -// export function bitAnd(left: Expr, right: Expr): BitAnd; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise AND operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise AND of 'field1' and 0xFF. -// * bitAnd(Field.of("field1"), 0xFF); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise AND operation. -// */ -// export function bitAnd(left: Expr, right: any): BitAnd; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise AND operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise AND of 'field1' and 'field2'. -// * bitAnd("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise AND operation. -// */ -// export function bitAnd(left: string, right: Expr): BitAnd; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise AND operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise AND of 'field1' and 0xFF. -// * bitAnd("field1", 0xFF); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise AND operation. -// */ -// export function bitAnd(left: string, right: any): BitAnd; -// export function bitAnd(left: Expr | string, right: Expr | any): BitAnd { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitAnd(normalizedLeft, normalizedRight); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise OR operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise OR of 'field1' and 'field2'. -// * bitOr(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise OR operation. -// */ -// export function bitOr(left: Expr, right: Expr): BitOr; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise OR operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise OR of 'field1' and 0xFF. -// * bitOr(Field.of("field1"), 0xFF); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise OR operation. -// */ -// export function bitOr(left: Expr, right: any): BitOr; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise OR operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise OR of 'field1' and 'field2'. -// * bitOr("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise OR operation. -// */ -// export function bitOr(left: string, right: Expr): BitOr; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise OR operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise OR of 'field1' and 0xFF. -// * bitOr("field1", 0xFF); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise OR operation. -// */ -// export function bitOr(left: string, right: any): BitOr; -// export function bitOr(left: Expr | string, right: Expr | any): BitOr { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitOr(normalizedLeft, normalizedRight); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise XOR operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise XOR of 'field1' and 'field2'. -// * bitXor(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise XOR operation. -// */ -// export function bitXor(left: Expr, right: Expr): BitXor; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise XOR operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise XOR of 'field1' and 0xFF. -// * bitXor(Field.of("field1"), 0xFF); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise XOR operation. -// */ -// export function bitXor(left: Expr, right: any): BitXor; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise XOR operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise XOR of 'field1' and 'field2'. -// * bitXor("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression. -// * @return A new {@code Expr} representing the bitwise XOR operation. -// */ -// export function bitXor(left: string, right: Expr): BitXor; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise XOR operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise XOR of 'field1' and 0xFF. -// * bitXor("field1", 0xFF); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant. -// * @return A new {@code Expr} representing the bitwise XOR operation. -// */ -// export function bitXor(left: string, right: any): BitXor; -// export function bitXor(left: Expr | string, right: Expr | any): BitXor { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitXor(normalizedLeft, normalizedRight); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise NOT operation to an expression. -// * -// * ```typescript -// * // Calculate the bitwise NOT of 'field1'. -// * bitNot(Field.of("field1")); -// * ``` -// * -// * @param operand The operand expression. -// * @return A new {@code Expr} representing the bitwise NOT operation. -// */ -// export function bitNot(operand: Expr): BitNot; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise NOT operation to a field. -// * -// * ```typescript -// * // Calculate the bitwise NOT of 'field1'. -// * bitNot("field1"); -// * ``` -// * -// * @param operand The operand field name. -// * @return A new {@code Expr} representing the bitwise NOT operation. -// */ -// export function bitNot(operand: string): BitNot; -// export function bitNot(operand: Expr | string): BitNot { -// const normalizedOperand = -// typeof operand === 'string' ? Field.of(operand) : operand; -// return new BitNot(normalizedOperand); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise left shift operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise left shift of 'field1' by 'field2' bits. -// * bitLeftShift(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise left shift operation. -// */ -// export function bitLeftShift(left: Expr, right: Expr): BitLeftShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise left shift operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise left shift of 'field1' by 2 bits. -// * bitLeftShift(Field.of("field1"), 2); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise left shift operation. -// */ -// export function bitLeftShift(left: Expr, right: any): BitLeftShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise left shift operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise left shift of 'field1' by 'field2' bits. -// * bitLeftShift("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise left shift operation. -// */ -// export function bitLeftShift(left: string, right: Expr): BitLeftShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise left shift operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise left shift of 'field1' by 2 bits. -// * bitLeftShift("field1", 2); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise left shift operation. -// */ -// export function bitLeftShift(left: string, right: any): BitLeftShift; -// export function bitLeftShift( -// left: Expr | string, -// right: Expr | any -// ): BitLeftShift { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitLeftShift(normalizedLeft, normalizedRight); -// } -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise right shift operation between two expressions. -// * -// * ```typescript -// * // Calculate the bitwise right shift of 'field1' by 'field2' bits. -// * bitRightShift(Field.of("field1"), Field.of("field2")); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand expression representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise right shift operation. -// */ -// export function bitRightShift(left: Expr, right: Expr): BitRightShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise right shift operation between an expression and a constant. -// * -// * ```typescript -// * // Calculate the bitwise right shift of 'field1' by 2 bits. -// * bitRightShift(Field.of("field1"), 2); -// * ``` -// * -// * @param left The left operand expression. -// * @param right The right operand constant representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise right shift operation. -// */ -// export function bitRightShift(left: Expr, right: any): BitRightShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise right shift operation between a field and an expression. -// * -// * ```typescript -// * // Calculate the bitwise right shift of 'field1' by 'field2' bits. -// * bitRightShift("field1", Field.of("field2")); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand expression representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise right shift operation. -// */ -// export function bitRightShift(left: string, right: Expr): BitRightShift; -// -// /** -// * @beta -// * -// * Creates an expression that applies a bitwise right shift operation between a field and a constant. -// * -// * ```typescript -// * // Calculate the bitwise right shift of 'field1' by 2 bits. -// * bitRightShift("field1", 2); -// * ``` -// * -// * @param left The left operand field name. -// * @param right The right operand constant representing the number of bits to shift. -// * @return A new {@code Expr} representing the bitwise right shift operation. -// */ -// export function bitRightShift(left: string, right: any): BitRightShift; -// export function bitRightShift( -// left: Expr | string, -// right: Expr | any -// ): BitRightShift { -// const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; -// const normalizedRight = right instanceof Expr ? right : Constant.of(right); -// return new BitRightShift(normalizedLeft, normalizedRight); -// } - -/** - * @beta - * - * Creates an expression that checks if two expressions are equal. - * - * ```typescript - * // Check if the 'age' field is equal to an expression - * eq(Field.of("age"), Field.of("minAge").add(10)); - * ``` - * - * @param left The first expression to compare. - * @param right The second expression to compare. - * @return A new `Expr` representing the equality comparison. - */ -export function eq(left: Expr, right: Expr): Eq; - -/** - * @beta - * - * Creates an expression that checks if an expression is equal to a constant value. - * - * ```typescript - * // Check if the 'age' field is equal to 21 - * eq(Field.of("age"), 21); - * ``` - * - * @param left The expression to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the equality comparison. - */ -export function eq(left: Expr, right: any): Eq; - -/** - * @beta - * - * Creates an expression that checks if a field's value is equal to an expression. - * - * ```typescript - * // Check if the 'age' field is equal to the 'limit' field - * eq("age", Field.of("limit")); - * ``` - * - * @param left The field name to compare. - * @param right The expression to compare to. - * @return A new `Expr` representing the equality comparison. - */ -export function eq(left: string, right: Expr): Eq; - -/** - * @beta - * - * Creates an expression that checks if a field's value is equal to a constant value. - * - * ```typescript - * // Check if the 'city' field is equal to string constant "London" - * eq("city", "London"); - * ``` - * - * @param left The field name to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the equality comparison. - */ -export function eq(left: string, right: any): Eq; -export function eq(left: Expr | string, right: any): Eq { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Eq(leftExpr, rightExpr); -} - -/** - * @beta - * - * Creates an expression that checks if two expressions are not equal. - * - * ```typescript - * // Check if the 'status' field is not equal to field 'finalState' - * neq(Field.of("status"), Field.of("finalState")); - * ``` - * - * @param left The first expression to compare. - * @param right The second expression to compare. - * @return A new `Expr` representing the inequality comparison. - */ -export function neq(left: Expr, right: Expr): Neq; - -/** - * @beta - * - * Creates an expression that checks if an expression is not equal to a constant value. - * - * ```typescript - * // Check if the 'status' field is not equal to "completed" - * neq(Field.of("status"), "completed"); - * ``` - * - * @param left The expression to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the inequality comparison. - */ -export function neq(left: Expr, right: any): Neq; - -/** - * @beta - * - * Creates an expression that checks if a field's value is not equal to an expression. - * - * ```typescript - * // Check if the 'status' field is not equal to the value of 'expectedStatus' - * neq("status", Field.of("expectedStatus")); - * ``` - * - * @param left The field name to compare. - * @param right The expression to compare to. - * @return A new `Expr` representing the inequality comparison. - */ -export function neq(left: string, right: Expr): Neq; - -/** - * @beta - * - * Creates an expression that checks if a field's value is not equal to a constant value. - * - * ```typescript - * // Check if the 'country' field is not equal to "USA" - * neq("country", "USA"); - * ``` - * - * @param left The field name to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the inequality comparison. - */ -export function neq(left: string, right: any): Neq; -export function neq(left: Expr | string, right: any): Neq { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Neq(leftExpr, rightExpr); -} - -/** - * @beta - * - * Creates an expression that checks if the first expression is less than the second expression. - * - * ```typescript - * // Check if the 'age' field is less than 30 - * lt(Field.of("age"), Field.of("limit")); - * ``` - * - * @param left The first expression to compare. - * @param right The second expression to compare. - * @return A new `Expr` representing the less than comparison. - */ -export function lt(left: Expr, right: Expr): Lt; - -/** - * @beta - * - * Creates an expression that checks if an expression is less than a constant value. - * - * ```typescript - * // Check if the 'age' field is less than 30 - * lt(Field.of("age"), 30); - * ``` - * - * @param left The expression to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the less than comparison. - */ -export function lt(left: Expr, right: any): Lt; - -/** - * @beta - * - * Creates an expression that checks if a field's value is less than an expression. - * - * ```typescript - * // Check if the 'age' field is less than the 'limit' field - * lt("age", Field.of("limit")); - * ``` - * - * @param left The field name to compare. - * @param right The expression to compare to. - * @return A new `Expr` representing the less than comparison. - */ -export function lt(left: string, right: Expr): Lt; - -/** - * @beta - * - * Creates an expression that checks if a field's value is less than a constant value. - * - * ```typescript - * // Check if the 'price' field is less than 50 - * lt("price", 50); - * ``` - * - * @param left The field name to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the less than comparison. - */ -export function lt(left: string, right: any): Lt; -export function lt(left: Expr | string, right: any): Lt { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Lt(leftExpr, rightExpr); -} - -/** - * @beta - * - * Creates an expression that checks if the first expression is less than or equal to the second - * expression. - * - * ```typescript - * // Check if the 'quantity' field is less than or equal to 20 - * lte(Field.of("quantity"), Field.of("limit")); - * ``` - * - * @param left The first expression to compare. - * @param right The second expression to compare. - * @return A new `Expr` representing the less than or equal to comparison. - */ -export function lte(left: Expr, right: Expr): Lte; - -/** - * @beta - * - * Creates an expression that checks if an expression is less than or equal to a constant value. - * - * ```typescript - * // Check if the 'quantity' field is less than or equal to 20 - * lte(Field.of("quantity"), 20); - * ``` - * - * @param left The expression to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the less than or equal to comparison. - */ -export function lte(left: Expr, right: any): Lte; - -/** - * Creates an expression that checks if a field's value is less than or equal to an expression. - * - * ```typescript - * // Check if the 'quantity' field is less than or equal to the 'limit' field - * lte("quantity", Field.of("limit")); - * ``` - * - * @param left The field name to compare. - * @param right The expression to compare to. - * @return A new `Expr` representing the less than or equal to comparison. - */ -export function lte(left: string, right: Expr): Lte; - -/** - * @beta - * - * Creates an expression that checks if a field's value is less than or equal to a constant value. - * - * ```typescript - * // Check if the 'score' field is less than or equal to 70 - * lte("score", 70); - * ``` - * - * @param left The field name to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the less than or equal to comparison. - */ -export function lte(left: string, right: any): Lte; -export function lte(left: Expr | string, right: any): Lte { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Lte(leftExpr, rightExpr); -} - -/** - * @beta - * - * Creates an expression that checks if the first expression is greater than the second - * expression. - * - * ```typescript - * // Check if the 'age' field is greater than 18 - * gt(Field.of("age"), Constant(9).add(9)); - * ``` - * - * @param left The first expression to compare. - * @param right The second expression to compare. - * @return A new `Expr` representing the greater than comparison. - */ -export function gt(left: Expr, right: Expr): Gt; - -/** - * @beta - * - * Creates an expression that checks if an expression is greater than a constant value. - * - * ```typescript - * // Check if the 'age' field is greater than 18 - * gt(Field.of("age"), 18); - * ``` - * - * @param left The expression to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the greater than comparison. - */ -export function gt(left: Expr, right: any): Gt; - -/** - * @beta - * - * Creates an expression that checks if a field's value is greater than an expression. - * - * ```typescript - * // Check if the value of field 'age' is greater than the value of field 'limit' - * gt("age", Field.of("limit")); - * ``` - * - * @param left The field name to compare. - * @param right The expression to compare to. - * @return A new `Expr` representing the greater than comparison. - */ -export function gt(left: string, right: Expr): Gt; - -/** - * @beta - * - * Creates an expression that checks if a field's value is greater than a constant value. - * - * ```typescript - * // Check if the 'price' field is greater than 100 - * gt("price", 100); - * ``` - * - * @param left The field name to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the greater than comparison. - */ -export function gt(left: string, right: any): Gt; -export function gt(left: Expr | string, right: any): Gt { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Gt(leftExpr, rightExpr); -} - -/** - * @beta - * - * Creates an expression that checks if the first expression is greater than or equal to the - * second expression. - * - * ```typescript - * // Check if the 'quantity' field is greater than or equal to the field "threshold" - * gte(Field.of("quantity"), Field.of("threshold")); - * ``` - * - * @param left The first expression to compare. - * @param right The second expression to compare. - * @return A new `Expr` representing the greater than or equal to comparison. - */ -export function gte(left: Expr, right: Expr): Gte; - -/** - * @beta - * - * Creates an expression that checks if an expression is greater than or equal to a constant - * value. - * - * ```typescript - * // Check if the 'quantity' field is greater than or equal to 10 - * gte(Field.of("quantity"), 10); - * ``` - * - * @param left The expression to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the greater than or equal to comparison. - */ -export function gte(left: Expr, right: any): Gte; - -/** - * @beta - * - * Creates an expression that checks if a field's value is greater than or equal to an expression. - * - * ```typescript - * // Check if the value of field 'age' is greater than or equal to the value of field 'limit' - * gte("age", Field.of("limit")); - * ``` - * - * @param left The field name to compare. - * @param right The expression to compare to. - * @return A new `Expr` representing the greater than or equal to comparison. - */ -export function gte(left: string, right: Expr): Gte; - -/** - * @beta - * - * Creates an expression that checks if a field's value is greater than or equal to a constant - * value. - * - * ```typescript - * // Check if the 'score' field is greater than or equal to 80 - * gte("score", 80); - * ``` - * - * @param left The field name to compare. - * @param right The constant value to compare to. - * @return A new `Expr` representing the greater than or equal to comparison. - */ -export function gte(left: string, right: any): Gte; -export function gte(left: Expr | string, right: any): Gte { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const rightExpr = right instanceof Expr ? right : Constant.of(right); - return new Gte(leftExpr, rightExpr); -} - -/** - * @beta - * - * Creates an expression that concatenates an array expression with other arrays. - * - * ```typescript - * // Combine the 'items' array with two new item arrays - * arrayConcat(Field.of("items"), [Field.of("newItems"), Field.of("otherItems")]); - * ``` - * - * @param array The array expression to concatenate to. - * @param elements The array expressions to concatenate. - * @return A new {@code Expr} representing the concatenated array. - */ -export function arrayConcat(array: Expr, elements: Expr[]): ArrayConcat; - -/** - * @beta - * - * Creates an expression that concatenates an array expression with other arrays and/or values. - * - * ```typescript - * // Combine the 'tags' array with a new array - * arrayConcat(Field.of("tags"), ["newTag1", "newTag2"]); - * ``` - * - * @param array The array expression to concatenate to. - * @param elements The array expressions or single values to concatenate. - * @return A new {@code Expr} representing the concatenated array. - */ -export function arrayConcat(array: Expr, elements: any[]): ArrayConcat; - -/** - * @beta - * - * Creates an expression that concatenates a field's array value with other arrays. - * - * ```typescript - * // Combine the 'items' array with two new item arrays - * arrayConcat("items", [Field.of("newItems"), Field.of("otherItems")]); - * ``` - * - * @param array The field name containing array values. - * @param elements The array expressions to concatenate. - * @return A new {@code Expr} representing the concatenated array. - */ -export function arrayConcat(array: string, elements: Expr[]): ArrayConcat; - -/** - * @beta - * - * Creates an expression that concatenates a field's array value with other arrays and/or values. - * - * ```typescript - * // Combine the 'tags' array with a new array - * arrayConcat("tags", ["newTag1", "newTag2"]); - * ``` - * - * @param array The field name containing array values. - * @param elements The array expressions or single values to concatenate. - * @return A new {@code Expr} representing the concatenated array. - */ -export function arrayConcat(array: string, elements: any[]): ArrayConcat; -export function arrayConcat( - array: Expr | string, - elements: any[] -): ArrayConcat { - const arrayExpr = array instanceof Expr ? array : Field.of(array); - const exprValues = elements.map(element => - element instanceof Expr ? element : Constant.of(element) - ); - return new ArrayConcat(arrayExpr, exprValues); -} - -/** - * @beta - * - * Creates an expression that checks if an array expression contains a specific element. - * - * ```typescript - * // Check if the 'colors' array contains the value of field 'selectedColor' - * arrayContains(Field.of("colors"), Field.of("selectedColor")); - * ``` - * - * @param array The array expression to check. - * @param element The element to search for in the array. - * @return A new {@code Expr} representing the 'array_contains' comparison. - */ -export function arrayContains(array: Expr, element: Expr): ArrayContains; - -/** - * @beta - * - * Creates an expression that checks if an array expression contains a specific element. - * - * ```typescript - * // Check if the 'colors' array contains "red" - * arrayContains(Field.of("colors"), "red"); - * ``` - * - * @param array The array expression to check. - * @param element The element to search for in the array. - * @return A new {@code Expr} representing the 'array_contains' comparison. - */ -export function arrayContains(array: Expr, element: any): ArrayContains; - -/** - * @beta - * - * Creates an expression that checks if a field's array value contains a specific element. - * - * ```typescript - * // Check if the 'colors' array contains the value of field 'selectedColor' - * arrayContains("colors", Field.of("selectedColor")); - * ``` - * - * @param array The field name to check. - * @param element The element to search for in the array. - * @return A new {@code Expr} representing the 'array_contains' comparison. - */ -export function arrayContains(array: string, element: Expr): ArrayContains; - -/** - * @beta - * - * Creates an expression that checks if a field's array value contains a specific value. - * - * ```typescript - * // Check if the 'colors' array contains "red" - * arrayContains("colors", "red"); - * ``` - * - * @param array The field name to check. - * @param element The element to search for in the array. - * @return A new {@code Expr} representing the 'array_contains' comparison. - */ -export function arrayContains(array: string, element: any): ArrayContains; -export function arrayContains( - array: Expr | string, - element: any -): ArrayContains { - const arrayExpr = array instanceof Expr ? array : Field.of(array); - const elementExpr = element instanceof Expr ? element : Constant.of(element); - return new ArrayContains(arrayExpr, elementExpr); -} - -/** - * @beta - * - * Creates an expression that checks if an array expression contains any of the specified - * elements. - * - * ```typescript - * // Check if the 'categories' array contains either values from field "cate1" or "Science" - * arrayContainsAny(Field.of("categories"), [Field.of("cate1"), "Science"]); - * ``` - * - * @param array The array expression to check. - * @param values The elements to check for in the array. - * @return A new {@code Expr} representing the 'array_contains_any' comparison. - */ -export function arrayContainsAny(array: Expr, values: Expr[]): ArrayContainsAny; - -/** - * @beta - * - * Creates an expression that checks if an array expression contains any of the specified - * elements. - * - * ```typescript - * // Check if the 'categories' array contains either values from field "cate1" or "Science" - * arrayContainsAny(Field.of("categories"), [Field.of("cate1"), "Science"]); - * ``` - * - * @param array The array expression to check. - * @param values The elements to check for in the array. - * @return A new {@code Expr} representing the 'array_contains_any' comparison. - */ -export function arrayContainsAny(array: Expr, values: any[]): ArrayContainsAny; - -/** - * @beta - * - * Creates an expression that checks if a field's array value contains any of the specified - * elements. - * - * ```typescript - * // Check if the 'groups' array contains either the value from the 'userGroup' field - * // or the value "guest" - * arrayContainsAny("categories", [Field.of("cate1"), "Science"]); - * ``` - * - * @param array The field name to check. - * @param values The elements to check for in the array. - * @return A new {@code Expr} representing the 'array_contains_any' comparison. - */ -export function arrayContainsAny( - array: string, - values: Expr[] -): ArrayContainsAny; - -/** - * @beta - * - * Creates an expression that checks if a field's array value contains any of the specified - * elements. - * - * ```typescript - * // Check if the 'groups' array contains either the value from the 'userGroup' field - * // or the value "guest" - * arrayContainsAny("categories", [Field.of("cate1"), "Science"]); - * ``` - * - * @param array The field name to check. - * @param values The elements to check for in the array. - * @return A new {@code Expr} representing the 'array_contains_any' comparison. - */ -export function arrayContainsAny( - array: string, - values: any[] -): ArrayContainsAny; -export function arrayContainsAny( - array: Expr | string, - values: any[] -): ArrayContainsAny { - const arrayExpr = array instanceof Expr ? array : Field.of(array); - const exprValues = values.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayContainsAny(arrayExpr, exprValues); -} - -/** - * @beta - * - * Creates an expression that checks if an array expression contains all the specified elements. - * - * ```typescript - * // Check if the 'tags' array contains both of the values from field 'tag1', 'tag2' and "tag3" - * arrayContainsAll(Field.of("tags"), [Field.of("tag1"), "SciFi", "Adventure"]); - * ``` - * - * @param array The array expression to check. - * @param values The elements to check for in the array. - * @return A new {@code Expr} representing the 'array_contains_all' comparison. - */ -export function arrayContainsAll(array: Expr, values: Expr[]): ArrayContainsAll; - -/** - * @beta - * - * Creates an expression that checks if an array expression contains all the specified elements. - * - * ```typescript - * // Check if the 'tags' array contains both of the values from field 'tag1', 'tag2' and "tag3" - * arrayContainsAll(Field.of("tags"), [Field.of("tag1"), "SciFi", "Adventure"]); - * ``` - * - * @param array The array expression to check. - * @param values The elements to check for in the array. - * @return A new {@code Expr} representing the 'array_contains_all' comparison. - */ -export function arrayContainsAll(array: Expr, values: any[]): ArrayContainsAll; - -/** - * @beta - * - * Creates an expression that checks if a field's array value contains all the specified values or - * expressions. - * - * ```typescript - * // Check if the 'tags' array contains both of the values from field 'tag1' and "tag2" - * arrayContainsAll("tags", [Field.of("tag1"), "SciFi", "Adventure"]); - * ``` - * - * @param array The field name to check. - * @param values The elements to check for in the array. - * @return A new {@code Expr} representing the 'array_contains_all' comparison. - */ -export function arrayContainsAll( - array: string, - values: Expr[] -): ArrayContainsAll; - -/** - * @beta - * - * Creates an expression that checks if a field's array value contains all the specified values or - * expressions. - * - * ```typescript - * // Check if the 'tags' array contains both of the values from field 'tag1' and "tag2" - * arrayContainsAll("tags", [Field.of("tag1"), "SciFi", "Adventure"]); - * ``` - * - * @param array The field name to check. - * @param values The elements to check for in the array. - * @return A new {@code Expr} representing the 'array_contains_all' comparison. - */ -export function arrayContainsAll( - array: string, - values: any[] -): ArrayContainsAll; -export function arrayContainsAll( - array: Expr | string, - values: any[] -): ArrayContainsAll { - const arrayExpr = array instanceof Expr ? array : Field.of(array); - const exprValues = values.map(value => - value instanceof Expr ? value : Constant.of(value) - ); - return new ArrayContainsAll(arrayExpr, exprValues); -} - -/** - * @beta - * - * Creates an expression that calculates the length of an array expression. - * - * ```typescript - * // Get the number of items in the 'cart' array - * arrayLength(Field.of("cart")); - * ``` - * - * @param array The array expression to calculate the length of. - * @return A new {@code Expr} representing the length of the array. - */ -export function arrayLength(array: Expr): ArrayLength { - return new ArrayLength(array); -} - -/** - * @beta - * - * Creates an expression that checks if an expression is equal to any of the provided values or - * expressions. - * - * ```typescript - * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * inAny(Field.of("category"), [Constant.of("Electronics"), Field.of("primaryType")]); - * ``` - * - * @param element The expression to compare. - * @param others The values to check against. - * @return A new {@code Expr} representing the 'IN' comparison. - */ -export function inAny(element: Expr, others: Expr[]): In; - -/** - * @beta - * - * Creates an expression that checks if an expression is equal to any of the provided values or - * expressions. - * - * ```typescript - * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * inAny(Field.of("category"), ["Electronics", Field.of("primaryType")]); - * ``` - * - * @param element The expression to compare. - * @param others The values to check against. - * @return A new {@code Expr} representing the 'IN' comparison. - */ -export function inAny(element: Expr, others: any[]): In; - -/** - * @beta - * - * Creates an expression that checks if a field's value is equal to any of the provided values or - * expressions. - * - * ```typescript - * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * inAny("category", [Constant.of("Electronics"), Field.of("primaryType")]); - * ``` - * - * @param element The field to compare. - * @param others The values to check against. - * @return A new {@code Expr} representing the 'IN' comparison. - */ -export function inAny(element: string, others: Expr[]): In; - -/** - * @beta - * - * Creates an expression that checks if a field's value is equal to any of the provided values or - * expressions. - * - * ```typescript - * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * inAny("category", ["Electronics", Field.of("primaryType")]); - * ``` - * - * @param element The field to compare. - * @param others The values to check against. - * @return A new {@code Expr} representing the 'IN' comparison. - */ -export function inAny(element: string, others: any[]): In; -export function inAny(element: Expr | string, others: any[]): In { - const elementExpr = element instanceof Expr ? element : Field.of(element); - const exprOthers = others.map(other => - other instanceof Expr ? other : Constant.of(other) - ); - return new In(elementExpr, exprOthers); -} - -/** - * @beta - * - * Creates an expression that checks if an expression is not equal to any of the provided values - * or expressions. - * - * ```typescript - * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notInAny(Field.of("status"), [Constant.of("pending"), Field.of("rejectedStatus")]); - * ``` - * - * @param element The expression to compare. - * @param others The values to check against. - * @return A new {@code Expr} representing the 'NOT IN' comparison. - */ -export function notInAny(element: Expr, others: Expr[]): Not; - -/** - * @beta - * - * Creates an expression that checks if an expression is not equal to any of the provided values - * or expressions. - * - * ```typescript - * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notInAny(Field.of("status"), ["pending", Field.of("rejectedStatus")]); - * ``` - * - * @param element The expression to compare. - * @param others The values to check against. - * @return A new {@code Expr} representing the 'NOT IN' comparison. - */ -export function notInAny(element: Expr, others: any[]): Not; - -/** - * @beta - * - * Creates an expression that checks if a field's value is not equal to any of the provided values - * or expressions. - * - * ```typescript - * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notInAny("status", [Constant.of("pending"), Field.of("rejectedStatus")]); - * ``` - * - * @param element The field name to compare. - * @param others The values to check against. - * @return A new {@code Expr} representing the 'NOT IN' comparison. - */ -export function notInAny(element: string, others: Expr[]): Not; - -/** - * @beta - * - * Creates an expression that checks if a field's value is not equal to any of the provided values - * or expressions. - * - * ```typescript - * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notInAny("status", ["pending", Field.of("rejectedStatus")]); - * ``` - * - * @param element The field name to compare. - * @param others The values to check against. - * @return A new {@code Expr} representing the 'NOT IN' comparison. - */ -export function notInAny(element: string, others: any[]): Not; -export function notInAny(element: Expr | string, others: any[]): Not { - const elementExpr = element instanceof Expr ? element : Field.of(element); - const exprOthers = others.map(other => - other instanceof Expr ? other : Constant.of(other) - ); - return new Not(new In(elementExpr, exprOthers)); -} - -/** - * @beta - * - * Creates an expression that performs a logical 'XOR' (exclusive OR) operation on multiple filter - * conditions. - * - * ```typescript - * // Check if only one of the conditions is true: 'age' greater than 18, 'city' is "London", - * // or 'status' is "active". - * const condition = xor( - * gt("age", 18), - * eq("city", "London"), - * eq("status", "active")); - * ``` - * - * @param left The first filter condition. - * @param right Additional filter conditions to 'XOR' together. - * @return A new {@code Expr} representing the logical 'XOR' operation. - */ -export function xor(left: FilterExpr, ...right: FilterExpr[]): Xor { - return new Xor([left, ...right]); -} - -/** - * @beta - * - * Creates a conditional expression that evaluates to a 'then' expression if a condition is true - * and an 'else' expression if the condition is false. - * - * ```typescript - * // If 'age' is greater than 18, return "Adult"; otherwise, return "Minor". - * ifFunction( - * gt("age", 18), Constant.of("Adult"), Constant.of("Minor")); - * ``` - * - * @param condition The condition to evaluate. - * @param thenExpr The expression to evaluate if the condition is true. - * @param elseExpr The expression to evaluate if the condition is false. - * @return A new {@code Expr} representing the conditional expression. - */ -export function ifFunction( - condition: FilterExpr, - thenExpr: Expr, - elseExpr: Expr -): If { - return new If(condition, thenExpr, elseExpr); -} - -/** - * @beta - * - * Creates an expression that negates a filter condition. - * - * ```typescript - * // Find documents where the 'completed' field is NOT true - * not(eq("completed", true)); - * ``` - * - * @param filter The filter condition to negate. - * @return A new {@code Expr} representing the negated filter condition. - */ -export function not(filter: FilterExpr): Not { - return new Not(filter); -} - -/** - * @beta - * - * Creates an expression that returns the larger value between two expressions, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the larger value between the 'field1' field and the 'field2' field. - * logicalMax(Field.of("field1"), Field.of("field2")); - * ``` - * - * @param left The left operand expression. - * @param right The right operand expression. - * @return A new {@code Expr} representing the logical max operation. - */ -export function logicalMax(left: Expr, right: Expr): LogicalMax; - -/** - * @beta - * - * Creates an expression that returns the larger value between an expression and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the larger value between the 'value' field and 10. - * logicalMax(Field.of("value"), 10); - * ``` - * - * @param left The left operand expression. - * @param right The right operand constant. - * @return A new {@code Expr} representing the logical max operation. - */ -export function logicalMax(left: Expr, right: any): LogicalMax; - -/** - * @beta - * - * Creates an expression that returns the larger value between a field and an expression, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the larger value between the 'field1' field and the 'field2' field. - * logicalMax("field1", Field.of('field2')); - * ``` - * - * @param left The left operand field name. - * @param right The right operand expression. - * @return A new {@code Expr} representing the logical max operation. - */ -export function logicalMax(left: string, right: Expr): LogicalMax; - -/** - * @beta - * - * Creates an expression that returns the larger value between a field and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the larger value between the 'value' field and 10. - * logicalMax("value", 10); - * ``` - * - * @param left The left operand field name. - * @param right The right operand constant. - * @return A new {@code Expr} representing the logical max operation. - */ -export function logicalMax(left: string, right: any): LogicalMax; -export function logicalMax(left: Expr | string, right: Expr | any): LogicalMax { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new LogicalMax(normalizedLeft, normalizedRight); -} - -/** - * @beta - * - * Creates an expression that returns the smaller value between two expressions, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the smaller value between the 'field1' field and the 'field2' field. - * logicalMin(Field.of("field1"), Field.of("field2")); - * ``` - * - * @param left The left operand expression. - * @param right The right operand expression. - * @return A new {@code Expr} representing the logical min operation. - */ -export function logicalMin(left: Expr, right: Expr): LogicalMin; - -/** - * @beta - * - * Creates an expression that returns the smaller value between an expression and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the smaller value between the 'value' field and 10. - * logicalMin(Field.of("value"), 10); - * ``` - * - * @param left The left operand expression. - * @param right The right operand constant. - * @return A new {@code Expr} representing the logical min operation. - */ -export function logicalMin(left: Expr, right: any): LogicalMin; - -/** - * @beta - * - * Creates an expression that returns the smaller value between a field and an expression, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the smaller value between the 'field1' field and the 'field2' field. - * logicalMin("field1", Field.of("field2")); - * ``` - * - * @param left The left operand field name. - * @param right The right operand expression. - * @return A new {@code Expr} representing the logical min operation. - */ -export function logicalMin(left: string, right: Expr): LogicalMin; - -/** - * @beta - * - * Creates an expression that returns the smaller value between a field and a constant value, based on Firestore's value type ordering. - * - * ```typescript - * // Returns the smaller value between the 'value' field and 10. - * logicalMin("value", 10); - * ``` - * - * @param left The left operand field name. - * @param right The right operand constant. - * @return A new {@code Expr} representing the logical min operation. - */ -export function logicalMin(left: string, right: any): LogicalMin; -export function logicalMin(left: Expr | string, right: Expr | any): LogicalMin { - const normalizedLeft = typeof left === 'string' ? Field.of(left) : left; - const normalizedRight = right instanceof Expr ? right : Constant.of(right); - return new LogicalMin(normalizedLeft, normalizedRight); -} - -/** - * @beta - * - * Creates an expression that checks if a field exists. - * - * ```typescript - * // Check if the document has a field named "phoneNumber" - * exists(Field.of("phoneNumber")); - * ``` - * - * @param value An expression evaluates to the name of the field to check. - * @return A new {@code Expr} representing the 'exists' check. - */ -export function exists(value: Expr): Exists; - -/** - * @beta - * - * Creates an expression that checks if a field exists. - * - * ```typescript - * // Check if the document has a field named "phoneNumber" - * exists("phoneNumber"); - * ``` - * - * @param field The field name to check. - * @return A new {@code Expr} representing the 'exists' check. - */ -export function exists(field: string): Exists; -export function exists(valueOrField: Expr | string): Exists { - const valueExpr = - valueOrField instanceof Expr ? valueOrField : Field.of(valueOrField); - return new Exists(valueExpr); -} - -/** - * @beta - * - * Creates an expression that checks if an expression evaluates to 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NaN - * isNaN(Field.of("value").divide(0)); - * ``` - * - * @param value The expression to check. - * @return A new {@code Expr} representing the 'isNaN' check. - */ -export function isNan(value: Expr): IsNan; - -/** - * @beta - * - * Creates an expression that checks if a field's value evaluates to 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NaN - * isNaN("value"); - * ``` - * - * @param value The name of the field to check. - * @return A new {@code Expr} representing the 'isNaN' check. - */ -export function isNan(value: string): IsNan; -export function isNan(value: Expr | string): IsNan { - const valueExpr = value instanceof Expr ? value : Field.of(value); - return new IsNan(valueExpr); -} - -/** - * @beta - * - * Creates an expression that reverses a string. - * - * ```typescript - * // Reverse the value of the 'myString' field. - * reverse(Field.of("myString")); - * ``` - * - * @param expr The expression representing the string to reverse. - * @return A new {@code Expr} representing the reversed string. - */ -export function reverse(expr: Expr): Reverse; - -/** - * @beta - * - * Creates an expression that reverses a string represented by a field. - * - * ```typescript - * // Reverse the value of the 'myString' field. - * reverse("myString"); - * ``` - * - * @param field The name of the field representing the string to reverse. - * @return A new {@code Expr} representing the reversed string. - */ -export function reverse(field: string): Reverse; -export function reverse(expr: Expr | string): Reverse { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new Reverse(normalizedExpr); -} - -/** - * @beta - * - * Creates an expression that replaces the first occurrence of a substring within a string with another substring. - * - * ```typescript - * // Replace the first occurrence of "hello" with "hi" in the 'message' field. - * replaceFirst(Field.of("message"), "hello", "hi"); - * ``` - * - * @param value The expression representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. - */ -export function replaceFirst( - value: Expr, - find: string, - replace: string -): ReplaceFirst; - -/** - * @beta - * - * Creates an expression that replaces the first occurrence of a substring within a string with another substring, - * where the substring to find and the replacement substring are specified by expressions. - * - * ```typescript - * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field. - * replaceFirst(Field.of("message"), Field.of("findField"), Field.of("replaceField")); - * ``` - * - * @param value The expression representing the string to perform the replacement on. - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. - */ -export function replaceFirst( - value: Expr, - find: Expr, - replace: Expr -): ReplaceFirst; - -/** - * @beta - * - * Creates an expression that replaces the first occurrence of a substring within a string represented by a field with another substring. - * - * ```typescript - * // Replace the first occurrence of "hello" with "hi" in the 'message' field. - * replaceFirst("message", "hello", "hi"); - * ``` - * - * @param field The name of the field representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. - */ -export function replaceFirst( - field: string, - find: string, - replace: string -): ReplaceFirst; -export function replaceFirst( - value: Expr | string, - find: Expr | string, - replace: Expr | string -): ReplaceFirst { - const normalizedValue = typeof value === 'string' ? Field.of(value) : value; - const normalizedFind = typeof find === 'string' ? Constant.of(find) : find; - const normalizedReplace = - typeof replace === 'string' ? Constant.of(replace) : replace; - return new ReplaceFirst(normalizedValue, normalizedFind, normalizedReplace); -} - -/** - * @beta - * - * Creates an expression that replaces all occurrences of a substring within a string with another substring. - * - * ```typescript - * // Replace all occurrences of "hello" with "hi" in the 'message' field. - * replaceAll(Field.of("message"), "hello", "hi"); - * ``` - * - * @param value The expression representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. - */ -export function replaceAll( - value: Expr, - find: string, - replace: string -): ReplaceAll; - -/** - * @beta - * - * Creates an expression that replaces all occurrences of a substring within a string with another substring, - * where the substring to find and the replacement substring are specified by expressions. - * - * ```typescript - * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field. - * replaceAll(Field.of("message"), Field.of("findField"), Field.of("replaceField")); - * ``` - * - * @param value The expression representing the string to perform the replacement on. - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. - */ -export function replaceAll(value: Expr, find: Expr, replace: Expr): ReplaceAll; - -/** - * @beta - * - * Creates an expression that replaces all occurrences of a substring within a string represented by a field with another substring. - * - * ```typescript - * // Replace all occurrences of "hello" with "hi" in the 'message' field. - * replaceAll("message", "hello", "hi"); - * ``` - * - * @param field The name of the field representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. - */ -export function replaceAll( - field: string, - find: string, - replace: string -): ReplaceAll; -export function replaceAll( - value: Expr | string, - find: Expr | string, - replace: Expr | string -): ReplaceAll { - const normalizedValue = typeof value === 'string' ? Field.of(value) : value; - const normalizedFind = typeof find === 'string' ? Constant.of(find) : find; - const normalizedReplace = - typeof replace === 'string' ? Constant.of(replace) : replace; - return new ReplaceAll(normalizedValue, normalizedFind, normalizedReplace); -} - -/** - * @beta - * - * Creates an expression that calculates the byte length of a string in UTF-8, or just the length of a Blob. - * - * ```typescript - * // Calculate the length of the 'myString' field in bytes. - * byteLength(Field.of("myString")); - * ``` - * - * @param expr The expression representing the string. - * @return A new {@code Expr} representing the length of the string in bytes. - */ -export function byteLength(expr: Expr): ByteLength; - -/** - * @beta - * - * Creates an expression that calculates the length of a string represented by a field in UTF-8 bytes, or just the length of a Blob. - * - * ```typescript - * // Calculate the length of the 'myString' field in bytes. - * byteLength("myString"); - * ``` - * - * @param field The name of the field representing the string. - * @return A new {@code Expr} representing the length of the string in bytes. - */ -export function byteLength(field: string): ByteLength; -export function byteLength(expr: Expr | string): ByteLength { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new ByteLength(normalizedExpr); -} - -/** - * @beta - * - * Creates an expression that calculates the character length of a string field in UTF8. - * - * ```typescript - * // Get the character length of the 'name' field in UTF-8. - * strLength("name"); - * ``` - * - * @param field The name of the field containing the string. - * @return A new {@code Expr} representing the length of the string. - */ -export function charLength(field: string): CharLength; - -/** - * @beta - * - * Creates an expression that calculates the character length of a string expression in UTF-8. - * - * ```typescript - * // Get the character length of the 'name' field in UTF-8. - * strLength(Field.of("name")); - * ``` - * - * @param expr The expression representing the string to calculate the length of. - * @return A new {@code Expr} representing the length of the string. - */ -export function charLength(expr: Expr): CharLength; -export function charLength(value: Expr | string): CharLength { - const valueExpr = value instanceof Expr ? value : Field.of(value); - return new CharLength(valueExpr); -} - -/** - * @beta - * - * Creates an expression that performs a case-sensitive wildcard string comparison against a - * field. - * - * ```typescript - * // Check if the 'title' field contains the string "guide" - * like("title", "%guide%"); - * ``` - * - * @param left The name of the field containing the string. - * @param pattern The pattern to search for. You can use "%" as a wildcard character. - * @return A new {@code Expr} representing the 'like' comparison. - */ -export function like(left: string, pattern: string): Like; - -/** - * @beta - * - * Creates an expression that performs a case-sensitive wildcard string comparison against a - * field. - * - * ```typescript - * // Check if the 'title' field contains the string "guide" - * like("title", Field.of("pattern")); - * ``` - * - * @param left The name of the field containing the string. - * @param pattern The pattern to search for. You can use "%" as a wildcard character. - * @return A new {@code Expr} representing the 'like' comparison. - */ -export function like(left: string, pattern: Expr): Like; - -/** - * @beta - * - * Creates an expression that performs a case-sensitive wildcard string comparison. - * - * ```typescript - * // Check if the 'title' field contains the string "guide" - * like(Field.of("title"), "%guide%"); - * ``` - * - * @param left The expression representing the string to perform the comparison on. - * @param pattern The pattern to search for. You can use "%" as a wildcard character. - * @return A new {@code Expr} representing the 'like' comparison. - */ -export function like(left: Expr, pattern: string): Like; - -/** - * @beta - * - * Creates an expression that performs a case-sensitive wildcard string comparison. - * - * ```typescript - * // Check if the 'title' field contains the string "guide" - * like(Field.of("title"), Field.of("pattern")); - * ``` - * - * @param left The expression representing the string to perform the comparison on. - * @param pattern The pattern to search for. You can use "%" as a wildcard character. - * @return A new {@code Expr} representing the 'like' comparison. - */ -export function like(left: Expr, pattern: Expr): Like; -export function like(left: Expr | string, pattern: Expr | string): Like { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const patternExpr = pattern instanceof Expr ? pattern : Constant.of(pattern); - return new Like(leftExpr, patternExpr); -} - -/** - * @beta - * - * Creates an expression that checks if a string field contains a specified regular expression as - * a substring. - * - * ```typescript - * // Check if the 'description' field contains "example" (case-insensitive) - * regexContains("description", "(?i)example"); - * ``` - * - * @param left The name of the field containing the string. - * @param pattern The regular expression to use for the search. - * @return A new {@code Expr} representing the 'contains' comparison. - */ -export function regexContains(left: string, pattern: string): RegexContains; - -/** - * @beta - * - * Creates an expression that checks if a string field contains a specified regular expression as - * a substring. - * - * ```typescript - * // Check if the 'description' field contains "example" (case-insensitive) - * regexContains("description", Field.of("pattern")); - * ``` - * - * @param left The name of the field containing the string. - * @param pattern The regular expression to use for the search. - * @return A new {@code Expr} representing the 'contains' comparison. - */ -export function regexContains(left: string, pattern: Expr): RegexContains; - -/** - * @beta - * - * Creates an expression that checks if a string expression contains a specified regular - * expression as a substring. - * - * ```typescript - * // Check if the 'description' field contains "example" (case-insensitive) - * regexContains(Field.of("description"), "(?i)example"); - * ``` - * - * @param left The expression representing the string to perform the comparison on. - * @param pattern The regular expression to use for the search. - * @return A new {@code Expr} representing the 'contains' comparison. - */ -export function regexContains(left: Expr, pattern: string): RegexContains; - -/** - * @beta - * - * Creates an expression that checks if a string expression contains a specified regular - * expression as a substring. - * - * ```typescript - * // Check if the 'description' field contains "example" (case-insensitive) - * regexContains(Field.of("description"), Field.of("pattern")); - * ``` - * - * @param left The expression representing the string to perform the comparison on. - * @param pattern The regular expression to use for the search. - * @return A new {@code Expr} representing the 'contains' comparison. - */ -export function regexContains(left: Expr, pattern: Expr): RegexContains; -export function regexContains( - left: Expr | string, - pattern: Expr | string -): RegexContains { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const patternExpr = pattern instanceof Expr ? pattern : Constant.of(pattern); - return new RegexContains(leftExpr, patternExpr); -} - -/** - * @beta - * - * Creates an expression that checks if a string field matches a specified regular expression. - * - * ```typescript - * // Check if the 'email' field matches a valid email pattern - * regexMatch("email", "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); - * ``` - * - * @param left The name of the field containing the string. - * @param pattern The regular expression to use for the match. - * @return A new {@code Expr} representing the regular expression match. - */ -export function regexMatch(left: string, pattern: string): RegexMatch; - -/** - * @beta - * - * Creates an expression that checks if a string field matches a specified regular expression. - * - * ```typescript - * // Check if the 'email' field matches a valid email pattern - * regexMatch("email", Field.of("pattern")); - * ``` - * - * @param left The name of the field containing the string. - * @param pattern The regular expression to use for the match. - * @return A new {@code Expr} representing the regular expression match. - */ -export function regexMatch(left: string, pattern: Expr): RegexMatch; - -/** - * @beta - * - * Creates an expression that checks if a string expression matches a specified regular - * expression. - * - * ```typescript - * // Check if the 'email' field matches a valid email pattern - * regexMatch(Field.of("email"), "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"); - * ``` - * - * @param left The expression representing the string to match against. - * @param pattern The regular expression to use for the match. - * @return A new {@code Expr} representing the regular expression match. - */ -export function regexMatch(left: Expr, pattern: string): RegexMatch; - -/** - * @beta - * - * Creates an expression that checks if a string expression matches a specified regular - * expression. - * - * ```typescript - * // Check if the 'email' field matches a valid email pattern - * regexMatch(Field.of("email"), Field.of("pattern")); - * ``` - * - * @param left The expression representing the string to match against. - * @param pattern The regular expression to use for the match. - * @return A new {@code Expr} representing the regular expression match. - */ -export function regexMatch(left: Expr, pattern: Expr): RegexMatch; -export function regexMatch( - left: Expr | string, - pattern: Expr | string -): RegexMatch { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const patternExpr = pattern instanceof Expr ? pattern : Constant.of(pattern); - return new RegexMatch(leftExpr, patternExpr); -} - -/** - * @beta - * - * Creates an expression that checks if a string field contains a specified substring. - * - * ```typescript - * // Check if the 'description' field contains "example". - * strContains("description", "example"); - * ``` - * - * @param left The name of the field containing the string. - * @param substring The substring to search for. - * @return A new {@code Expr} representing the 'contains' comparison. - */ -export function strContains(left: string, substring: string): StrContains; - -/** - * @beta - * - * Creates an expression that checks if a string field contains a substring specified by an expression. - * - * ```typescript - * // Check if the 'description' field contains the value of the 'keyword' field. - * strContains("description", Field.of("keyword")); - * ``` - * - * @param left The name of the field containing the string. - * @param substring The expression representing the substring to search for. - * @return A new {@code Expr} representing the 'contains' comparison. - */ -export function strContains(left: string, substring: Expr): StrContains; - -/** - * @beta - * - * Creates an expression that checks if a string expression contains a specified substring. - * - * ```typescript - * // Check if the 'description' field contains "example". - * strContains(Field.of("description"), "example"); - * ``` - * - * @param left The expression representing the string to perform the comparison on. - * @param substring The substring to search for. - * @return A new {@code Expr} representing the 'contains' comparison. - */ -export function strContains(left: Expr, substring: string): StrContains; - -/** - * @beta - * - * Creates an expression that checks if a string expression contains a substring specified by another expression. - * - * ```typescript - * // Check if the 'description' field contains the value of the 'keyword' field. - * strContains(Field.of("description"), Field.of("keyword")); - * ``` - * - * @param left The expression representing the string to perform the comparison on. - * @param substring The expression representing the substring to search for. - * @return A new {@code Expr} representing the 'contains' comparison. - */ -export function strContains(left: Expr, substring: Expr): StrContains; -export function strContains( - left: Expr | string, - substring: Expr | string -): StrContains { - const leftExpr = left instanceof Expr ? left : Field.of(left); - const substringExpr = - substring instanceof Expr ? substring : Constant.of(substring); - return new StrContains(leftExpr, substringExpr); -} - -/** - * @beta - * - * Creates an expression that checks if a field's value starts with a given prefix. - * - * ```typescript - * // Check if the 'name' field starts with "Mr." - * startsWith("name", "Mr."); - * ``` - * - * @param expr The field name to check. - * @param prefix The prefix to check for. - * @return A new {@code Expr} representing the 'starts with' comparison. - */ -export function startsWith(expr: string, prefix: string): StartsWith; - -/** - * @beta - * - * Creates an expression that checks if a field's value starts with a given prefix. - * - * ```typescript - * // Check if the 'fullName' field starts with the value of the 'firstName' field - * startsWith("fullName", Field.of("firstName")); - * ``` - * - * @param expr The field name to check. - * @param prefix The expression representing the prefix. - * @return A new {@code Expr} representing the 'starts with' comparison. - */ -export function startsWith(expr: string, prefix: Expr): StartsWith; - -/** - * @beta - * - * Creates an expression that checks if a string expression starts with a given prefix. - * - * ```typescript - * // Check if the result of concatenating 'firstName' and 'lastName' fields starts with "Mr." - * startsWith(Field.of("fullName"), "Mr."); - * ``` - * - * @param expr The expression to check. - * @param prefix The prefix to check for. - * @return A new {@code Expr} representing the 'starts with' comparison. - */ -export function startsWith(expr: Expr, prefix: string): StartsWith; - -/** - * @beta - * - * Creates an expression that checks if a string expression starts with a given prefix. - * - * ```typescript - * // Check if the result of concatenating 'firstName' and 'lastName' fields starts with "Mr." - * startsWith(Field.of("fullName"), Field.of("prefix")); - * ``` - * - * @param expr The expression to check. - * @param prefix The prefix to check for. - * @return A new {@code Expr} representing the 'starts with' comparison. - */ -export function startsWith(expr: Expr, prefix: Expr): StartsWith; -export function startsWith( - expr: Expr | string, - prefix: Expr | string -): StartsWith { - const exprLeft = expr instanceof Expr ? expr : Field.of(expr); - const prefixExpr = prefix instanceof Expr ? prefix : Constant.of(prefix); - return new StartsWith(exprLeft, prefixExpr); -} - -/** - * @beta - * - * Creates an expression that checks if a field's value ends with a given postfix. - * - * ```typescript - * // Check if the 'filename' field ends with ".txt" - * endsWith("filename", ".txt"); - * ``` - * - * @param expr The field name to check. - * @param suffix The postfix to check for. - * @return A new {@code Expr} representing the 'ends with' comparison. - */ -export function endsWith(expr: string, suffix: string): EndsWith; - -/** - * @beta - * - * Creates an expression that checks if a field's value ends with a given postfix. - * - * ```typescript - * // Check if the 'url' field ends with the value of the 'extension' field - * endsWith("url", Field.of("extension")); - * ``` - * - * @param expr The field name to check. - * @param suffix The expression representing the postfix. - * @return A new {@code Expr} representing the 'ends with' comparison. - */ -export function endsWith(expr: string, suffix: Expr): EndsWith; - -/** - * @beta - * - * Creates an expression that checks if a string expression ends with a given postfix. - * - * ```typescript - * // Check if the result of concatenating 'firstName' and 'lastName' fields ends with "Jr." - * endsWith(Field.of("fullName"), "Jr."); - * ``` - * - * @param expr The expression to check. - * @param suffix The postfix to check for. - * @return A new {@code Expr} representing the 'ends with' comparison. - */ -export function endsWith(expr: Expr, suffix: string): EndsWith; - -/** - * @beta - * - * Creates an expression that checks if a string expression ends with a given postfix. - * - * ```typescript - * // Check if the result of concatenating 'firstName' and 'lastName' fields ends with "Jr." - * endsWith(Field.of("fullName"), Constant.of("Jr.")); - * ``` - * - * @param expr The expression to check. - * @param suffix The postfix to check for. - * @return A new {@code Expr} representing the 'ends with' comparison. - */ -export function endsWith(expr: Expr, suffix: Expr): EndsWith; -export function endsWith(expr: Expr | string, suffix: Expr | string): EndsWith { - const exprLeft = expr instanceof Expr ? expr : Field.of(expr); - const suffixExpr = suffix instanceof Expr ? suffix : Constant.of(suffix); - return new EndsWith(exprLeft, suffixExpr); -} - -/** - * @beta - * - * Creates an expression that converts a string field to lowercase. - * - * ```typescript - * // Convert the 'name' field to lowercase - * toLower("name"); - * ``` - * - * @param expr The name of the field containing the string. - * @return A new {@code Expr} representing the lowercase string. - */ -export function toLower(expr: string): ToLower; - -/** - * @beta - * - * Creates an expression that converts a string expression to lowercase. - * - * ```typescript - * // Convert the 'name' field to lowercase - * toLower(Field.of("name")); - * ``` - * - * @param expr The expression representing the string to convert to lowercase. - * @return A new {@code Expr} representing the lowercase string. - */ -export function toLower(expr: Expr): ToLower; -export function toLower(expr: Expr | string): ToLower { - return new ToLower(expr instanceof Expr ? expr : Field.of(expr)); -} - -/** - * @beta - * - * Creates an expression that converts a string field to uppercase. - * - * ```typescript - * // Convert the 'title' field to uppercase - * toUpper("title"); - * ``` - * - * @param expr The name of the field containing the string. - * @return A new {@code Expr} representing the uppercase string. - */ -export function toUpper(expr: string): ToUpper; - -/** - * @beta - * - * Creates an expression that converts a string expression to uppercase. - * - * ```typescript - * // Convert the 'title' field to uppercase - * toUppercase(Field.of("title")); - * ``` - * - * @param expr The expression representing the string to convert to uppercase. - * @return A new {@code Expr} representing the uppercase string. - */ -export function toUpper(expr: Expr): ToUpper; -export function toUpper(expr: Expr | string): ToUpper { - return new ToUpper(expr instanceof Expr ? expr : Field.of(expr)); -} - -/** - * @beta - * - * Creates an expression that removes leading and trailing whitespace from a string field. - * - * ```typescript - * // Trim whitespace from the 'userInput' field - * trim("userInput"); - * ``` - * - * @param expr The name of the field containing the string. - * @return A new {@code Expr} representing the trimmed string. - */ -export function trim(expr: string): Trim; - -/** - * @beta - * - * Creates an expression that removes leading and trailing whitespace from a string expression. - * - * ```typescript - * // Trim whitespace from the 'userInput' field - * trim(Field.of("userInput")); - * ``` - * - * @param expr The expression representing the string to trim. - * @return A new {@code Expr} representing the trimmed string. - */ -export function trim(expr: Expr): Trim; -export function trim(expr: Expr | string): Trim { - return new Trim(expr instanceof Expr ? expr : Field.of(expr)); -} - -/** - * @beta - * - * Creates an expression that concatenates string functions, fields or constants together. - * - * ```typescript - * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * strConcat("firstName", " ", Field.of("lastName")); - * ``` - * - * @param first The field name containing the initial string value. - * @param elements The expressions (typically strings) to concatenate. - * @return A new {@code Expr} representing the concatenated string. - */ -export function strConcat( - first: string, - ...elements: Array -): StrConcat; - -/** - * @beta - * Creates an expression that concatenates string expressions together. - * - * ```typescript - * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * strConcat(Field.of("firstName"), " ", Field.of("lastName")); - * ``` - * - * @param first The initial string expression to concatenate to. - * @param elements The expressions (typically strings) to concatenate. - * @return A new {@code Expr} representing the concatenated string. - */ -export function strConcat( - first: Expr, - ...elements: Array -): StrConcat; -export function strConcat( - first: string | Expr, - ...elements: Array -): StrConcat { - const exprs = elements.map(e => (e instanceof Expr ? e : Constant.of(e))); - return new StrConcat(first instanceof Expr ? first : Field.of(first), exprs); -} - -/** - * @beta - * - * Accesses a value from a map (object) field using the provided key. - * - * ```typescript - * // Get the 'city' value from the 'address' map field - * mapGet("address", "city"); - * ``` - * - * @param mapField The field name of the map field. - * @param subField The key to access in the map. - * @return A new {@code Expr} representing the value associated with the given key in the map. - */ -export function mapGet(mapField: string, subField: string): MapGet; - -/** - * @beta - * - * Accesses a value from a map (object) expression using the provided key. - * - * ```typescript - * // Get the 'city' value from the 'address' map field - * mapGet(Field.of("address"), "city"); - * ``` - * - * @param mapExpr The expression representing the map. - * @param subField The key to access in the map. - * @return A new {@code Expr} representing the value associated with the given key in the map. - */ -export function mapGet(mapExpr: Expr, subField: string): MapGet; -export function mapGet(fieldOrExpr: string | Expr, subField: string): MapGet { - return new MapGet( - typeof fieldOrExpr === 'string' ? Field.of(fieldOrExpr) : fieldOrExpr, - subField - ); -} - -/** - * @beta - * - * Creates an aggregation that counts the total number of stage inputs. - * - * ```typescript - * // Count the total number of users - * countAll().as("totalUsers"); - * ``` - * - * @return A new {@code Accumulator} representing the 'countAll' aggregation. - */ -export function countAll(): Count { - return new Count(undefined, false); -} - -/** - * @beta - * - * Creates an aggregation that counts the number of stage inputs with valid evaluations of the - * provided expression. - * - * ```typescript - * // Count the number of items where the price is greater than 10 - * count(Field.of("price").gt(10)).as("expensiveItemCount"); - * ``` - * - * @param value The expression to count. - * @return A new {@code Accumulator} representing the 'count' aggregation. - */ -export function countFunction(value: Expr): Count; - -/** - * Creates an aggregation that counts the number of stage inputs with valid evaluations of the - * provided field. - * - * ```typescript - * // Count the total number of products - * count("productId").as("totalProducts"); - * ``` - * - * @param value The name of the field to count. - * @return A new {@code Accumulator} representing the 'count' aggregation. - */ -export function countFunction(value: string): Count; -export function countFunction(value: Expr | string): Count { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Count(exprValue, false); -} - -/** - * @beta - * - * Creates an aggregation that calculates the sum of values from an expression across multiple - * stage inputs. - * - * ```typescript - * // Calculate the total revenue from a set of orders - * sum(Field.of("orderAmount")).as("totalRevenue"); - * ``` - * - * @param value The expression to sum up. - * @return A new {@code Accumulator} representing the 'sum' aggregation. - */ -export function sumFunction(value: Expr): Sum; - -/** - * @beta - * - * Creates an aggregation that calculates the sum of a field's values across multiple stage - * inputs. - * - * ```typescript - * // Calculate the total revenue from a set of orders - * sum("orderAmount").as("totalRevenue"); - * ``` - * - * @param value The name of the field containing numeric values to sum up. - * @return A new {@code Accumulator} representing the 'sum' aggregation. - */ -export function sumFunction(value: string): Sum; -export function sumFunction(value: Expr | string): Sum { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Sum(exprValue, false); -} - -/** - * @beta - * - * Creates an aggregation that calculates the average (mean) of values from an expression across - * multiple stage inputs. - * - * ```typescript - * // Calculate the average age of users - * avg(Field.of("age")).as("averageAge"); - * ``` - * - * @param value The expression representing the values to average. - * @return A new {@code Accumulator} representing the 'avg' aggregation. - */ -export function avgFunction(value: Expr): Avg; - -/** - * @beta - * - * Creates an aggregation that calculates the average (mean) of a field's values across multiple - * stage inputs. - * - * ```typescript - * // Calculate the average age of users - * avg("age").as("averageAge"); - * ``` - * - * @param value The name of the field containing numeric values to average. - * @return A new {@code Accumulator} representing the 'avg' aggregation. - */ -export function avgFunction(value: string): Avg; -export function avgFunction(value: Expr | string): Avg { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Avg(exprValue, false); -} - -/** - * @beta - * - * Creates an aggregation that finds the minimum value of an expression across multiple stage - * inputs. - * - * ```typescript - * // Find the lowest price of all products - * min(Field.of("price")).as("lowestPrice"); - * ``` - * - * @param value The expression to find the minimum value of. - * @return A new {@code Accumulator} representing the 'min' aggregation. - */ -export function min(value: Expr): Min; - -/** - * @beta - * - * Creates an aggregation that finds the minimum value of a field across multiple stage inputs. - * - * ```typescript - * // Find the lowest price of all products - * min("price").as("lowestPrice"); - * ``` - * - * @param value The name of the field to find the minimum value of. - * @return A new {@code Accumulator} representing the 'min' aggregation. - */ -export function min(value: string): Min; -export function min(value: Expr | string): Min { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Min(exprValue, false); -} - -/** - * @beta - * - * Creates an aggregation that finds the maximum value of an expression across multiple stage - * inputs. - * - * ```typescript - * // Find the highest score in a leaderboard - * max(Field.of("score")).as("highestScore"); - * ``` - * - * @param value The expression to find the maximum value of. - * @return A new {@code Accumulator} representing the 'max' aggregation. - */ -export function max(value: Expr): Max; - -/** - * @beta - * - * Creates an aggregation that finds the maximum value of a field across multiple stage inputs. - * - * ```typescript - * // Find the highest score in a leaderboard - * max("score").as("highestScore"); - * ``` - * - * @param value The name of the field to find the maximum value of. - * @return A new {@code Accumulator} representing the 'max' aggregation. - */ -export function max(value: string): Max; -export function max(value: Expr | string): Max { - const exprValue = value instanceof Expr ? value : Field.of(value); - return new Max(exprValue, false); -} - -/** - * @beta - * - * Calculates the Cosine distance between a field's vector value and a double array. - * - * ```typescript - * // Calculate the Cosine distance between the 'location' field and a target location - * cosineDistance("location", [37.7749, -122.4194]); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as an array of doubles) to compare against. - * @return A new {@code Expr} representing the Cosine distance between the two vectors. - */ -export function cosineDistance(expr: string, other: number[]): CosineDistance; - -/** - * @beta - * - * Calculates the Cosine distance between a field's vector value and a VectorValue. - * - * ```typescript - * // Calculate the Cosine distance between the 'location' field and a target location - * cosineDistance("location", new VectorValue([37.7749, -122.4194])); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as a VectorValue) to compare against. - * @return A new {@code Expr} representing the Cosine distance between the two vectors. - */ -export function cosineDistance( - expr: string, - other: VectorValue -): CosineDistance; - -/** - * @beta - * - * Calculates the Cosine distance between a field's vector value and a vector expression. - * - * ```typescript - * // Calculate the cosine distance between the 'userVector' field and the 'itemVector' field - * cosineDistance("userVector", Field.of("itemVector")); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (represented as an Expr) to compare against. - * @return A new {@code Expr} representing the cosine distance between the two vectors. - */ -export function cosineDistance(expr: string, other: Expr): CosineDistance; - -/** - * @beta - * - * Calculates the Cosine distance between a vector expression and a double array. - * - * ```typescript - * // Calculate the cosine distance between the 'location' field and a target location - * cosineDistance(Field.of("location"), [37.7749, -122.4194]); - * ``` - * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (as an array of doubles) to compare against. - * @return A new {@code Expr} representing the cosine distance between the two vectors. - */ -export function cosineDistance(expr: Expr, other: number[]): CosineDistance; - -/** - * @beta - * - * Calculates the Cosine distance between a vector expression and a VectorValue. - * - * ```typescript - * // Calculate the cosine distance between the 'location' field and a target location - * cosineDistance(Field.of("location"), new VectorValue([37.7749, -122.4194])); - * ``` - * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (as a VectorValue) to compare against. - * @return A new {@code Expr} representing the cosine distance between the two vectors. - */ -export function cosineDistance(expr: Expr, other: VectorValue): CosineDistance; - -/** - * @beta - * - * Calculates the Cosine distance between two vector expressions. - * - * ```typescript - * // Calculate the cosine distance between the 'userVector' field and the 'itemVector' field - * cosineDistance(Field.of("userVector"), Field.of("itemVector")); - * ``` - * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (represented as an Expr) to compare against. - * @return A new {@code Expr} representing the cosine distance between the two vectors. - */ -export function cosineDistance(expr: Expr, other: Expr): CosineDistance; -export function cosineDistance( - expr: Expr | string, - other: Expr | number[] | VectorValue -): CosineDistance { - const expr1 = expr instanceof Expr ? expr : Field.of(expr); - const expr2 = other instanceof Expr ? other : Constant.vector(other); - return new CosineDistance(expr1, expr2); -} - -/** - * @beta - * - * Calculates the dot product between a field's vector value and a double array. - * - * ```typescript - * // Calculate the dot product distance between a feature vector and a target vector - * dotProduct("features", [0.5, 0.8, 0.2]); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as an array of doubles) to calculate with. - * @return A new {@code Expr} representing the dot product between the two vectors. - */ -export function dotProduct(expr: string, other: number[]): DotProduct; - -/** - * @beta - * - * Calculates the dot product between a field's vector value and a VectorValue. - * - * ```typescript - * // Calculate the dot product distance between a feature vector and a target vector - * dotProduct("features", new VectorValue([0.5, 0.8, 0.2])); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as a VectorValue) to calculate with. - * @return A new {@code Expr} representing the dot product between the two vectors. - */ -export function dotProduct(expr: string, other: VectorValue): DotProduct; - -/** - * @beta - * - * Calculates the dot product between a field's vector value and a vector expression. - * - * ```typescript - * // Calculate the dot product distance between two document vectors: 'docVector1' and 'docVector2' - * dotProduct("docVector1", Field.of("docVector2")); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (represented as an Expr) to calculate with. - * @return A new {@code Expr} representing the dot product between the two vectors. - */ -export function dotProduct(expr: string, other: Expr): DotProduct; - -/** - * @beta - * - * Calculates the dot product between a vector expression and a double array. - * - * ```typescript - * // Calculate the dot product between a feature vector and a target vector - * dotProduct(Field.of("features"), [0.5, 0.8, 0.2]); - * ``` - * - * @param expr The first vector (represented as an Expr) to calculate with. - * @param other The other vector (as an array of doubles) to calculate with. - * @return A new {@code Expr} representing the dot product between the two vectors. - */ -export function dotProduct(expr: Expr, other: number[]): DotProduct; - -/** - * @beta - * - * Calculates the dot product between a vector expression and a VectorValue. - * - * ```typescript - * // Calculate the dot product between a feature vector and a target vector - * dotProduct(Field.of("features"), new VectorValue([0.5, 0.8, 0.2])); - * ``` - * - * @param expr The first vector (represented as an Expr) to calculate with. - * @param other The other vector (as a VectorValue) to calculate with. - * @return A new {@code Expr} representing the dot product between the two vectors. - */ -export function dotProduct(expr: Expr, other: VectorValue): DotProduct; - -/** - * @beta - * - * Calculates the dot product between two vector expressions. - * - * ```typescript - * // Calculate the dot product between two document vectors: 'docVector1' and 'docVector2' - * dotProduct(Field.of("docVector1"), Field.of("docVector2")); - * ``` - * - * @param expr The first vector (represented as an Expr) to calculate with. - * @param other The other vector (represented as an Expr) to calculate with. - * @return A new {@code Expr} representing the dot product between the two vectors. - */ -export function dotProduct(expr: Expr, other: Expr): DotProduct; -export function dotProduct( - expr: Expr | string, - other: Expr | number[] | VectorValue -): DotProduct { - const expr1 = expr instanceof Expr ? expr : Field.of(expr); - const expr2 = other instanceof Expr ? other : Constant.vector(other); - return new DotProduct(expr1, expr2); -} - -/** - * @beta - * - * Calculates the Euclidean distance between a field's vector value and a double array. - * - * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * euclideanDistance("location", [37.7749, -122.4194]); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as an array of doubles) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. - */ -export function euclideanDistance( - expr: string, - other: number[] -): EuclideanDistance; - -/** - * @beta - * - * Calculates the Euclidean distance between a field's vector value and a VectorValue. - * - * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * euclideanDistance("location", new VectorValue([37.7749, -122.4194])); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (as a VectorValue) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. - */ -export function euclideanDistance( - expr: string, - other: VectorValue -): EuclideanDistance; - -/** - * @beta - * - * Calculates the Euclidean distance between a field's vector value and a vector expression. - * - * ```typescript - * // Calculate the Euclidean distance between two vector fields: 'pointA' and 'pointB' - * euclideanDistance("pointA", Field.of("pointB")); - * ``` - * - * @param expr The name of the field containing the first vector. - * @param other The other vector (represented as an Expr) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. - */ -export function euclideanDistance(expr: string, other: Expr): EuclideanDistance; - -/** - * @beta - * - * Calculates the Euclidean distance between a vector expression and a double array. - * - * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * - * euclideanDistance(Field.of("location"), [37.7749, -122.4194]); - * ``` - * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (as an array of doubles) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. - */ -export function euclideanDistance( - expr: Expr, - other: number[] -): EuclideanDistance; - -/** - * @beta - * - * Calculates the Euclidean distance between a vector expression and a VectorValue. - * - * ```typescript - * // Calculate the Euclidean distance between the 'location' field and a target location - * euclideanDistance(Field.of("location"), new VectorValue([37.7749, -122.4194])); - * ``` - * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (as a VectorValue) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. - */ -export function euclideanDistance( - expr: Expr, - other: VectorValue -): EuclideanDistance; - -/** - * @beta - * - * Calculates the Euclidean distance between two vector expressions. - * - * ```typescript - * // Calculate the Euclidean distance between two vector fields: 'pointA' and 'pointB' - * euclideanDistance(Field.of("pointA"), Field.of("pointB")); - * ``` - * - * @param expr The first vector (represented as an Expr) to compare against. - * @param other The other vector (represented as an Expr) to compare against. - * @return A new {@code Expr} representing the Euclidean distance between the two vectors. - */ -export function euclideanDistance(expr: Expr, other: Expr): EuclideanDistance; -export function euclideanDistance( - expr: Expr | string, - other: Expr | number[] | VectorValue -): EuclideanDistance { - const expr1 = expr instanceof Expr ? expr : Field.of(expr); - const expr2 = other instanceof Expr ? other : Constant.vector(other); - return new EuclideanDistance(expr1, expr2); -} - -/** - * @beta - * - * Creates an expression that calculates the length of a Firestore Vector. - * - * ```typescript - * // Get the vector length (dimension) of the field 'embedding'. - * vectorLength(Field.of("embedding")); - * ``` - * - * @param expr The expression representing the Firestore Vector. - * @return A new {@code Expr} representing the length of the array. - */ -export function vectorLength(expr: Expr): VectorLength; - -/** - * @beta - * - * Creates an expression that calculates the length of a Firestore Vector represented by a field. - * - * ```typescript - * // Get the vector length (dimension) of the field 'embedding'. - * vectorLength("embedding"); - * ``` - * - * @param field The name of the field representing the Firestore Vector. - * @return A new {@code Expr} representing the length of the array. - */ -export function vectorLength(field: string): VectorLength; -export function vectorLength(expr: Expr | string): VectorLength { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new VectorLength(normalizedExpr); -} - -/** - * @beta - * - * Creates an expression that interprets an expression as the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC) - * and returns a timestamp. - * - * ```typescript - * // Interpret the 'microseconds' field as microseconds since epoch. - * unixMicrosToTimestamp(Field.of("microseconds")); - * ``` - * - * @param expr The expression representing the number of microseconds since epoch. - * @return A new {@code Expr} representing the timestamp. - */ -export function unixMicrosToTimestamp(expr: Expr): UnixMicrosToTimestamp; - -/** - * @beta - * - * Creates an expression that interprets a field's value as the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC) - * and returns a timestamp. - * - * ```typescript - * // Interpret the 'microseconds' field as microseconds since epoch. - * unixMicrosToTimestamp("microseconds"); - * ``` - * - * @param field The name of the field representing the number of microseconds since epoch. - * @return A new {@code Expr} representing the timestamp. - */ -export function unixMicrosToTimestamp(field: string): UnixMicrosToTimestamp; -export function unixMicrosToTimestamp( - expr: Expr | string -): UnixMicrosToTimestamp { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new UnixMicrosToTimestamp(normalizedExpr); -} - -/** - * @beta - * - * Creates an expression that converts a timestamp expression to the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC). - * - * ```typescript - * // Convert the 'timestamp' field to microseconds since epoch. - * timestampToUnixMicros(Field.of("timestamp")); - * ``` - * - * @param expr The expression representing the timestamp. - * @return A new {@code Expr} representing the number of microseconds since epoch. - */ -export function timestampToUnixMicros(expr: Expr): TimestampToUnixMicros; - -/** - * @beta - * - * Creates an expression that converts a timestamp field to the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC). - * - * ```typescript - * // Convert the 'timestamp' field to microseconds since epoch. - * timestampToUnixMicros("timestamp"); - * ``` - * - * @param field The name of the field representing the timestamp. - * @return A new {@code Expr} representing the number of microseconds since epoch. - */ -export function timestampToUnixMicros(field: string): TimestampToUnixMicros; -export function timestampToUnixMicros( - expr: Expr | string -): TimestampToUnixMicros { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new TimestampToUnixMicros(normalizedExpr); -} - -/** - * @beta - * - * Creates an expression that interprets an expression as the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC) - * and returns a timestamp. - * - * ```typescript - * // Interpret the 'milliseconds' field as milliseconds since epoch. - * unixMillisToTimestamp(Field.of("milliseconds")); - * ``` - * - * @param expr The expression representing the number of milliseconds since epoch. - * @return A new {@code Expr} representing the timestamp. - */ -export function unixMillisToTimestamp(expr: Expr): UnixMillisToTimestamp; - -/** - * @beta - * - * Creates an expression that interprets a field's value as the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC) - * and returns a timestamp. - * - * ```typescript - * // Interpret the 'milliseconds' field as milliseconds since epoch. - * unixMillisToTimestamp("milliseconds"); - * ``` - * - * @param field The name of the field representing the number of milliseconds since epoch. - * @return A new {@code Expr} representing the timestamp. - */ -export function unixMillisToTimestamp(field: string): UnixMillisToTimestamp; -export function unixMillisToTimestamp( - expr: Expr | string -): UnixMillisToTimestamp { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new UnixMillisToTimestamp(normalizedExpr); -} - -/** - * @beta - * - * Creates an expression that converts a timestamp expression to the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC). - * - * ```typescript - * // Convert the 'timestamp' field to milliseconds since epoch. - * timestampToUnixMillis(Field.of("timestamp")); - * ``` - * - * @param expr The expression representing the timestamp. - * @return A new {@code Expr} representing the number of milliseconds since epoch. - */ -export function timestampToUnixMillis(expr: Expr): TimestampToUnixMillis; - -/** - * @beta - * - * Creates an expression that converts a timestamp field to the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC). - * - * ```typescript - * // Convert the 'timestamp' field to milliseconds since epoch. - * timestampToUnixMillis("timestamp"); - * ``` - * - * @param field The name of the field representing the timestamp. - * @return A new {@code Expr} representing the number of milliseconds since epoch. - */ -export function timestampToUnixMillis(field: string): TimestampToUnixMillis; -export function timestampToUnixMillis( - expr: Expr | string -): TimestampToUnixMillis { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new TimestampToUnixMillis(normalizedExpr); -} - -/** - * @beta - * - * Creates an expression that interprets an expression as the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC) - * and returns a timestamp. - * - * ```typescript - * // Interpret the 'seconds' field as seconds since epoch. - * unixSecondsToTimestamp(Field.of("seconds")); - * ``` - * - * @param expr The expression representing the number of seconds since epoch. - * @return A new {@code Expr} representing the timestamp. - */ -export function unixSecondsToTimestamp(expr: Expr): UnixSecondsToTimestamp; - -/** - * @beta - * - * Creates an expression that interprets a field's value as the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC) - * and returns a timestamp. - * - * ```typescript - * // Interpret the 'seconds' field as seconds since epoch. - * unixSecondsToTimestamp("seconds"); - * ``` - * - * @param field The name of the field representing the number of seconds since epoch. - * @return A new {@code Expr} representing the timestamp. - */ -export function unixSecondsToTimestamp(field: string): UnixSecondsToTimestamp; -export function unixSecondsToTimestamp( - expr: Expr | string -): UnixSecondsToTimestamp { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new UnixSecondsToTimestamp(normalizedExpr); -} - -/** - * @beta - * - * Creates an expression that converts a timestamp expression to the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC). - * - * ```typescript - * // Convert the 'timestamp' field to seconds since epoch. - * timestampToUnixSeconds(Field.of("timestamp")); - * ``` - * - * @param expr The expression representing the timestamp. - * @return A new {@code Expr} representing the number of seconds since epoch. - */ -export function timestampToUnixSeconds(expr: Expr): TimestampToUnixSeconds; - -/** - * @beta - * - * Creates an expression that converts a timestamp field to the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC). - * - * ```typescript - * // Convert the 'timestamp' field to seconds since epoch. - * timestampToUnixSeconds("timestamp"); - * ``` - * - * @param field The name of the field representing the timestamp. - * @return A new {@code Expr} representing the number of seconds since epoch. - */ -export function timestampToUnixSeconds(field: string): TimestampToUnixSeconds; -export function timestampToUnixSeconds( - expr: Expr | string -): TimestampToUnixSeconds { - const normalizedExpr = typeof expr === 'string' ? Field.of(expr) : expr; - return new TimestampToUnixSeconds(normalizedExpr); -} - -/** - * @beta - * - * Creates an expression that adds a specified amount of time to a timestamp. - * - * ```typescript - * // Add some duration determined by field 'unit' and 'amount' to the 'timestamp' field. - * timestampAdd(Field.of("timestamp"), Field.of("unit"), Field.of("amount")); - * ``` - * - * @param timestamp The expression representing the timestamp. - * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. - * @param amount The expression evaluates to amount of the unit. - * @return A new {@code Expr} representing the resulting timestamp. - */ -export function timestampAdd( - timestamp: Expr, - unit: Expr, - amount: Expr -): TimestampAdd; - -/** - * @beta - * - * Creates an expression that adds a specified amount of time to a timestamp. - * - * ```typescript - * // Add 1 day to the 'timestamp' field. - * timestampAdd(Field.of("timestamp"), "day", 1); - * ``` - * - * @param timestamp The expression representing the timestamp. - * @param unit The unit of time to add (e.g., "day", "hour"). - * @param amount The amount of time to add. - * @return A new {@code Expr} representing the resulting timestamp. - */ -export function timestampAdd( - timestamp: Expr, - unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: number -): TimestampAdd; - -/** - * @beta - * - * Creates an expression that adds a specified amount of time to a timestamp represented by a field. - * - * ```typescript - * // Add 1 day to the 'timestamp' field. - * timestampAdd("timestamp", "day", 1); - * ``` - * - * @param field The name of the field representing the timestamp. - * @param unit The unit of time to add (e.g., "day", "hour"). - * @param amount The amount of time to add. - * @return A new {@code Expr} representing the resulting timestamp. - */ -export function timestampAdd( - field: string, - unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: number -): TimestampAdd; -export function timestampAdd( - timestamp: Expr | string, - unit: - | Expr - | 'microsecond' - | 'millisecond' - | 'second' - | 'minute' - | 'hour' - | 'day', - amount: Expr | number -): TimestampAdd { - const normalizedTimestamp = - typeof timestamp === 'string' ? Field.of(timestamp) : timestamp; - const normalizedUnit = unit instanceof Expr ? unit : Constant.of(unit); - const normalizedAmount = - typeof amount === 'number' ? Constant.of(amount) : amount; - return new TimestampAdd( - normalizedTimestamp, - normalizedUnit, - normalizedAmount - ); -} - -/** - * @beta - * - * Creates an expression that subtracts a specified amount of time from a timestamp. - * - * ```typescript - * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. - * timestampSub(Field.of("timestamp"), Field.of("unit"), Field.of("amount")); - * ``` - * - * @param timestamp The expression representing the timestamp. - * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. - * @param amount The expression evaluates to amount of the unit. - * @return A new {@code Expr} representing the resulting timestamp. - */ -export function timestampSub( - timestamp: Expr, - unit: Expr, - amount: Expr -): TimestampSub; - -/** - * @beta - * - * Creates an expression that subtracts a specified amount of time from a timestamp. - * - * ```typescript - * // Subtract 1 day from the 'timestamp' field. - * timestampSub(Field.of("timestamp"), "day", 1); - * ``` - * - * @param timestamp The expression representing the timestamp. - * @param unit The unit of time to subtract (e.g., "day", "hour"). - * @param amount The amount of time to subtract. - * @return A new {@code Expr} representing the resulting timestamp. - */ -export function timestampSub( - timestamp: Expr, - unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: number -): TimestampSub; - -/** - * @beta - * - * Creates an expression that subtracts a specified amount of time from a timestamp represented by a field. - * - * ```typescript - * // Subtract 1 day from the 'timestamp' field. - * timestampSub("timestamp", "day", 1); - * ``` - * - * @param field The name of the field representing the timestamp. - * @param unit The unit of time to subtract (e.g., "day", "hour"). - * @param amount The amount of time to subtract. - * @return A new {@code Expr} representing the resulting timestamp. - */ -export function timestampSub( - field: string, - unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: number -): TimestampSub; -export function timestampSub( - timestamp: Expr | string, - unit: - | Expr - | 'microsecond' - | 'millisecond' - | 'second' - | 'minute' - | 'hour' - | 'day', - amount: Expr | number -): TimestampSub { - const normalizedTimestamp = - typeof timestamp === 'string' ? Field.of(timestamp) : timestamp; - const normalizedUnit = unit instanceof Expr ? unit : Constant.of(unit); - const normalizedAmount = - typeof amount === 'number' ? Constant.of(amount) : amount; - return new TimestampSub( - normalizedTimestamp, - normalizedUnit, - normalizedAmount - ); -} - -/** - * @beta - * - * Creates functions that work on the backend but do not exist in the SDK yet. - * - * ```typescript - * // Call a user defined function named "myFunc" with the arguments 10 and 20 - * // This is the same of the 'sum(Field.of("price"))', if it did not exist - * genericFunction("sum", [Field.of("price")]); - * ``` - * - * @param name The name of the user defined function. - * @param params The arguments to pass to the function. - * @return A new {@code Function} representing the function call. - */ -export function genericFunction( - name: string, - params: Expr[] -): FirestoreFunction { - return new FirestoreFunction(name, params); -} - -/** - * @beta - * - * Creates an expression that performs a logical 'AND' operation on multiple filter conditions. - * - * ```typescript - * // Check if the 'age' field is greater than 18 AND the 'city' field is "London" AND - * // the 'status' field is "active" - * const condition = and(gt("age", 18), eq("city", "London"), eq("status", "active")); - * ``` - * - * @param left The first filter condition. - * @param right Additional filter conditions to 'AND' together. - * @return A new {@code Expr} representing the logical 'AND' operation. - */ -export function andFunction(left: FilterExpr, ...right: FilterExpr[]): And { - return new And([left, ...right]); -} - -/** - * @beta - * - * Creates an expression that performs a logical 'OR' operation on multiple filter conditions. - * - * ```typescript - * // Check if the 'age' field is greater than 18 OR the 'city' field is "London" OR - * // the 'status' field is "active" - * const condition = or(gt("age", 18), eq("city", "London"), eq("status", "active")); - * ``` - * - * @param left The first filter condition. - * @param right Additional filter conditions to 'OR' together. - * @return A new {@code Expr} representing the logical 'OR' operation. - */ -export function orFunction(left: FilterExpr, ...right: FilterExpr[]): Or { - return new Or([left, ...right]); -} - -/** - * @beta - * - * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. - * - * ```typescript - * // Sort documents by the 'name' field in ascending order - * firestore.pipeline().collection("users") - * .sort(ascending(Field.of("name"))); - * ``` - * - * @param expr The expression to create an ascending ordering for. - * @return A new `Ordering` for ascending sorting. - */ -export function ascending(expr: Expr): Ordering { - return new Ordering(expr, 'ascending'); -} - -/** - * @beta - * - * Creates an {@link Ordering} that sorts documents in descending order based on this expression. - * - * ```typescript - * // Sort documents by the 'createdAt' field in descending order - * firestore.pipeline().collection("users") - * .sort(descending(Field.of("createdAt"))); - * ``` - * - * @param expr The expression to create a descending ordering for. - * @return A new `Ordering` for descending sorting. - */ -export function descending(expr: Expr): Ordering { - return new Ordering(expr, 'descending'); -} - -/** - * @beta - * - * Represents an ordering criterion for sorting documents in a Firestore pipeline. - * - * You create `Ordering` instances using the `ascending` and `descending` helper functions. - */ -export class Ordering { - constructor( - private expr: Expr, - private direction: 'ascending' | 'descending' - ) {} - - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { - return { - mapValue: { - fields: { - direction: toStringValue(this.direction), - expression: this.expr._toProto(serializer) - } - } - }; - } - - /** - * @private - * @internal - */ - _readUserData(dataReader: UserDataReader): void { - this.expr._readUserData(dataReader); - } -} diff --git a/packages/firestore/src/lite-api/pipeline-result.ts b/packages/firestore/src/lite-api/pipeline-result.ts deleted file mode 100644 index f3951099ba2..00000000000 --- a/packages/firestore/src/lite-api/pipeline-result.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ObjectValue } from '../model/object_value'; -import { isOptionalEqual } from '../util/misc'; - -import { FieldPath } from './field_path'; -import { DocumentData, DocumentReference, refEqual } from './reference'; -import { fieldPathFromArgument } from './snapshot'; -import { Timestamp } from './timestamp'; -import { AbstractUserDataWriter } from './user_data_writer'; - -/** - * @beta - * - * A PipelineResult contains data read from a Firestore Pipeline. The data can be extracted with the - * {@link #data()} or {@link #get(String)} methods. - * - *

    If the PipelineResult represents a non-document result, `ref` will return a undefined - * value. - */ -export class PipelineResult { - private readonly _userDataWriter: AbstractUserDataWriter; - - private readonly _executionTime: Timestamp | undefined; - private readonly _createTime: Timestamp | undefined; - private readonly _updateTime: Timestamp | undefined; - - /** - * @internal - * @private - */ - readonly _ref: DocumentReference | undefined; - - /** - * @internal - * @private - */ - readonly _fields: ObjectValue | undefined; - - /** - * @private - * @internal - * - * @param userDataWriter The serializer used to encode/decode protobuf. - * @param ref The reference to the document. - * @param _fieldsProto The fields of the Firestore `Document` Protobuf backing - * this document (or undefined if the document does not exist). - * @param readTime The time when this result was read (or undefined if - * the document exists only locally). - * @param createTime The time when the document was created if the result is a document, undefined otherwise. - * @param updateTime The time when the document was last updated if the result is a document, undefined otherwise. - */ - constructor( - userDataWriter: AbstractUserDataWriter, - ref?: DocumentReference, - fields?: ObjectValue, - executionTime?: Timestamp, - createTime?: Timestamp, - updateTime?: Timestamp - // TODO converter - //readonly converter: FirestorePipelineConverter = defaultPipelineConverter() - ) { - this._ref = ref; - this._userDataWriter = userDataWriter; - this._executionTime = executionTime; - this._createTime = createTime; - this._updateTime = updateTime; - this._fields = fields; - } - - /** - * The reference of the document, if it is a document; otherwise `undefined`. - */ - get ref(): DocumentReference | undefined { - return this._ref; - } - - /** - * The ID of the document for which this PipelineResult contains data, if it is a document; otherwise `undefined`. - * - * @type {string} - * @readonly - * - */ - get id(): string | undefined { - return this._ref?.id; - } - - /** - * The time the document was created. Undefined if this result is not a document. - * - * @type {Timestamp|undefined} - * @readonly - */ - get createTime(): Timestamp | undefined { - return this._createTime; - } - - /** - * The time the document was last updated (at the time the snapshot was - * generated). Undefined if this result is not a document. - * - * @type {Timestamp|undefined} - * @readonly - */ - get updateTime(): Timestamp | undefined { - return this._updateTime; - } - - /** - * The time at which the pipeline producing this result is executed. - * - * @type {Timestamp} - * @readonly - * - */ - get executionTime(): Timestamp { - if (this._executionTime === undefined) { - throw new Error( - "'executionTime' is expected to exist, but it is undefined" - ); - } - return this._executionTime; - } - - /** - * Retrieves all fields in the result as an object. Returns 'undefined' if - * the document doesn't exist. - * - * @returns {T|undefined} An object containing all fields in the document or - * 'undefined' if the document doesn't exist. - * - * @example - * ``` - * let p = firestore.pipeline().collection('col'); - * - * p.execute().then(results => { - * let data = results[0].data(); - * console.log(`Retrieved data: ${JSON.stringify(data)}`); - * }); - * ``` - */ - data(): AppModelType | undefined { - if (this._fields === undefined) { - return undefined; - } - - // TODO(pipelines) - // We only want to use the converter and create a new QueryDocumentSnapshot - // if a converter has been provided. - // if (!!this.converter && this.converter !== defaultPipelineConverter()) { - // return this.converter.fromFirestore( - // new PipelineResult< DocumentData>( - // this._serializer, - // this.ref, - // this._fieldsProto, - // this._executionTime, - // this.createTime, - // this.updateTime, - // defaultPipelineConverter() - // ) - // ); - // } else {{ - return this._userDataWriter.convertValue( - this._fields.value - ) as AppModelType; - //} - } - - /** - * Retrieves the field specified by `field`. - * - * @param {string|FieldPath} field The field path - * (e.g. 'foo' or 'foo.bar') to a specific field. - * @returns {*} The data at the specified field location or undefined if no - * such field exists. - * - * @example - * ``` - * let p = firestore.pipeline().collection('col'); - * - * p.execute().then(results => { - * let field = results[0].get('a.b'); - * console.log(`Retrieved field value: ${field}`); - * }); - * ``` - */ - // We deliberately use `any` in the external API to not impose type-checking - // on end users. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get(fieldPath: string | FieldPath): any { - if (this._fields === undefined) { - return undefined; - } - - const value = this._fields.field( - fieldPathFromArgument('DocumentSnapshot.get', fieldPath) - ); - if (value !== null) { - return this._userDataWriter.convertValue(value); - } - } -} - -export function pipelineResultEqual( - left: PipelineResult, - right: PipelineResult -): boolean { - if (left === right) { - return true; - } - - return ( - isOptionalEqual(left._ref, right._ref, refEqual) && - isOptionalEqual(left._fields, right._fields, (l, r) => l.isEqual(r)) - ); -} diff --git a/packages/firestore/src/lite-api/pipeline-source.ts b/packages/firestore/src/lite-api/pipeline-source.ts deleted file mode 100644 index a79c5cafc31..00000000000 --- a/packages/firestore/src/lite-api/pipeline-source.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DocumentKey } from '../model/document_key'; - -import { Firestore } from './database'; -import { Pipeline } from './pipeline'; -import { DocumentReference } from './reference'; -import { - CollectionGroupSource, - CollectionSource, - DatabaseSource, - DocumentsSource -} from './stage'; -import { UserDataReader } from './user_data_reader'; -import { AbstractUserDataWriter } from './user_data_writer'; - -/** - * Represents the source of a Firestore {@link Pipeline}. - * @beta - */ -export class PipelineSource { - /** - * @internal - * @private - * @param db - * @param userDataReader - * @param userDataWriter - * @param documentReferenceFactory - */ - constructor( - private db: Firestore, - private userDataReader: UserDataReader, - private userDataWriter: AbstractUserDataWriter, - private documentReferenceFactory: (id: DocumentKey) => DocumentReference - ) {} - - collection(collectionPath: string): Pipeline { - return new Pipeline( - this.db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - [new CollectionSource(collectionPath)] - ); - } - - collectionGroup(collectionId: string): Pipeline { - return new Pipeline( - this.db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - [new CollectionGroupSource(collectionId)] - ); - } - - database(): Pipeline { - return new Pipeline( - this.db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - [new DatabaseSource()] - ); - } - - documents(docs: DocumentReference[]): Pipeline { - return new Pipeline( - this.db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - [DocumentsSource.of(docs)] - ); - } -} diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts deleted file mode 100644 index 4d13d5dfddd..00000000000 --- a/packages/firestore/src/lite-api/pipeline.ts +++ /dev/null @@ -1,853 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* eslint @typescript-eslint/no-explicit-any: 0 */ - -import { DocumentKey } from '../model/document_key'; -import { ObjectValue } from '../model/object_value'; -import { - ExecutePipelineRequest, - StructuredPipeline, - Stage as ProtoStage -} from '../protos/firestore_proto_api'; -import { invokeExecutePipeline } from '../remote/datastore'; -import { - getEncodedDatabaseId, - JsonProtoSerializer, - ProtoSerializable -} from '../remote/serializer'; - -import { getDatastore } from './components'; -import { Firestore } from './database'; -import { - Accumulator, - AccumulatorTarget, - Expr, - ExprWithAlias, - Field, - Fields, - FilterCondition, - Ordering, - Selectable -} from './expressions'; -import { PipelineResult } from './pipeline-result'; -import { PipelineSource } from './pipeline-source'; -import { DocumentData, DocumentReference, Query } from './reference'; -import { - AddFields, - Aggregate, - Distinct, - FindNearest, - FindNearestOptions, - GenericStage, - Limit, - Offset, - Select, - Sort, - Stage, - Where -} from './stage'; -import { - parseVectorValue, - UserDataReader, - UserDataSource -} from './user_data_reader'; -import { AbstractUserDataWriter } from './user_data_writer'; - -interface ReadableUserData { - _readUserData(dataReader: UserDataReader): void; -} - -function isReadableUserData(value: any): value is ReadableUserData { - return typeof (value as ReadableUserData)._readUserData === 'function'; -} - -/** - * @beta - * - * The Pipeline class provides a flexible and expressive framework for building complex data - * transformation and query pipelines for Firestore. - * - * A pipeline takes data sources, such as Firestore collections or collection groups, and applies - * a series of stages that are chained together. Each stage takes the output from the previous stage - * (or the data source) and produces an output for the next stage (or as the final output of the - * pipeline). - * - * Expressions can be used within each stage to filter and transform data through the stage. - * - * NOTE: The chained stages do not prescribe exactly how Firestore will execute the pipeline. - * Instead, Firestore only guarantees that the result is the same as if the chained stages were - * executed in order. - * - * Usage Examples: - * - * ```typescript - * const db: Firestore; // Assumes a valid firestore instance. - * - * // Example 1: Select specific fields and rename 'rating' to 'bookRating' - * const results1 = await db.pipeline() - * .collection("books") - * .select("title", "author", Field.of("rating").as("bookRating")) - * .execute(); - * - * // Example 2: Filter documents where 'genre' is "Science Fiction" and 'published' is after 1950 - * const results2 = await db.pipeline() - * .collection("books") - * .where(and(Field.of("genre").eq("Science Fiction"), Field.of("published").gt(1950))) - * .execute(); - * - * // Example 3: Calculate the average rating of books published after 1980 - * const results3 = await db.pipeline() - * .collection("books") - * .where(Field.of("published").gt(1980)) - * .aggregate(avg(Field.of("rating")).as("averageRating")) - * .execute(); - * ``` - */ - -/** - * Base-class implementation - */ -export class Pipeline - implements ProtoSerializable -{ - /** - * @internal - * @private - * @param _db - * @param userDataReader - * @param userDataWriter - * @param documentReferenceFactory - * @param stages - * @param converter - */ - constructor( - /** - * @internal - * @private - */ - public _db: Firestore, - private userDataReader: UserDataReader, - /** - * @internal - * @private - */ - protected userDataWriter: AbstractUserDataWriter, - /** - * @internal - * @private - */ - protected documentReferenceFactory: (id: DocumentKey) => DocumentReference, - private stages: Stage[], - // TODO(pipeline) support converter - //private converter: FirestorePipelineConverter = defaultPipelineConverter() - private converter: unknown = {} - ) {} - - /** - * Adds new fields to outputs from previous stages. - * - * This stage allows you to compute values on-the-fly based on existing data from previous - * stages or constants. You can use this to create new fields or overwrite existing ones (if there - * is name overlaps). - * - * The added fields are defined using {@link Selectable}s, which can be: - * - * - {@link Field}: References an existing document field. - * - {@link Function}: Performs a calculation using functions like `add`, `multiply` with - * assigned aliases using {@link Expr#as}. - * - * Example: - * - * ```typescript - * firestore.pipeline().collection("books") - * .addFields( - * Field.of("rating").as("bookRating"), // Rename 'rating' to 'bookRating' - * add(5, Field.of("quantity")).as("totalCost") // Calculate 'totalCost' - * ); - * ``` - * - * @param fields The fields to add to the documents, specified as {@link Selectable}s. - * @return A new Pipeline object with this stage appended to the stage list. - */ - addFields(...fields: Selectable[]): Pipeline { - const copy = this.stages.map(s => s); - copy.push( - new AddFields( - this.readUserData('addFields', this.selectablesToMap(fields)) - ) - ); - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy, - this.converter - ); - } - - /** - * Selects or creates a set of fields from the outputs of previous stages. - * - *

    The selected fields are defined using {@link Selectable} expressions, which can be: - * - *

      - *
    • {@code string}: Name of an existing field
    • - *
    • {@link Field}: References an existing field.
    • - *
    • {@link Function}: Represents the result of a function with an assigned alias name using - * {@link Expr#as}
    • - *
    - * - *

    If no selections are provided, the output of this stage is empty. Use {@link - * com.google.cloud.firestore.Pipeline#addFields} instead if only additions are - * desired. - * - *

    Example: - * - * ```typescript - * firestore.pipeline().collection("books") - * .select( - * "firstName", - * Field.of("lastName"), - * Field.of("address").toUppercase().as("upperAddress"), - * ); - * ``` - * - * @param selections The fields to include in the output documents, specified as {@link - * Selectable} expressions or {@code string} values representing field names. - * @return A new Pipeline object with this stage appended to the stage list. - */ - select(...selections: Array): Pipeline { - const copy = this.stages.map(s => s); - let projections: Map = this.selectablesToMap(selections); - projections = this.readUserData('select', projections); - copy.push(new Select(projections)); - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy, - this.converter - ); - } - - private selectablesToMap( - selectables: Array - ): Map { - const result = new Map(); - for (const selectable of selectables) { - if (typeof selectable === 'string') { - result.set(selectable as string, Field.of(selectable)); - } else if (selectable instanceof Field) { - result.set((selectable as Field).fieldName(), selectable); - } else if (selectable instanceof Fields) { - const fields = selectable as Fields; - for (const field of fields.fieldList()) { - result.set(field.fieldName(), field); - } - } else if (selectable instanceof ExprWithAlias) { - const expr = selectable as ExprWithAlias; - result.set(expr.alias, expr.expr); - } - } - return result; - } - - /** - * Reads user data for each expression in the expressionMap. - * @param name Name of the calling function. Used for error messages when invalid user data is encountered. - * @param expressionMap - * @return the expressionMap argument. - * @private - */ - private readUserData< - T extends - | Map - | ReadableUserData[] - | ReadableUserData - >(name: string, expressionMap: T): T { - if (isReadableUserData(expressionMap)) { - expressionMap._readUserData(this.userDataReader); - } else if (Array.isArray(expressionMap)) { - expressionMap.forEach(readableData => - readableData._readUserData(this.userDataReader) - ); - } else { - expressionMap.forEach(expr => expr._readUserData(this.userDataReader)); - } - return expressionMap; - } - - /** - * Filters the documents from previous stages to only include those matching the specified {@link - * FilterCondition}. - * - *

    This stage allows you to apply conditions to the data, similar to a "WHERE" clause in SQL. - * You can filter documents based on their field values, using implementations of {@link - * FilterCondition}, typically including but not limited to: - * - *

      - *
    • field comparators: {@link Function#eq}, {@link Function#lt} (less than), {@link - * Function#gt} (greater than), etc.
    • - *
    • logical operators: {@link Function#and}, {@link Function#or}, {@link Function#not}, etc.
    • - *
    • advanced functions: {@link Function#regexMatch}, {@link - * Function#arrayContains}, etc.
    • - *
    - * - *

    Example: - * - * ```typescript - * firestore.pipeline().collection("books") - * .where( - * and( - * gt(Field.of("rating"), 4.0), // Filter for ratings greater than 4.0 - * Field.of("genre").eq("Science Fiction") // Equivalent to gt("genre", "Science Fiction") - * ) - * ); - * ``` - * - * @param condition The {@link FilterCondition} to apply. - * @return A new Pipeline object with this stage appended to the stage list. - */ - where(condition: FilterCondition & Expr): Pipeline { - const copy = this.stages.map(s => s); - this.readUserData('where', condition); - copy.push(new Where(condition)); - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy, - this.converter - ); - } - - /** - * Skips the first `offset` number of documents from the results of previous stages. - * - *

    This stage is useful for implementing pagination in your pipelines, allowing you to retrieve - * results in chunks. It is typically used in conjunction with {@link #limit} to control the - * size of each page. - * - *

    Example: - * - * ```typescript - * // Retrieve the second page of 20 results - * firestore.pipeline().collection("books") - * .sort(Field.of("published").descending()) - * .offset(20) // Skip the first 20 results - * .limit(20); // Take the next 20 results - * ``` - * - * @param offset The number of documents to skip. - * @return A new Pipeline object with this stage appended to the stage list. - */ - offset(offset: number): Pipeline { - const copy = this.stages.map(s => s); - copy.push(new Offset(offset)); - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy, - this.converter - ); - } - - /** - * Limits the maximum number of documents returned by previous stages to `limit`. - * - *

    This stage is particularly useful when you want to retrieve a controlled subset of data from - * a potentially large result set. It's often used for: - * - *

      - *
    • **Pagination:** In combination with {@link #offset} to retrieve specific pages of - * results.
    • - *
    • **Limiting Data Retrieval:** To prevent excessive data transfer and improve performance, - * especially when dealing with large collections.
    • - *
    - * - *

    Example: - * - * ```typescript - * // Limit the results to the top 10 highest-rated books - * firestore.pipeline().collection("books") - * .sort(Field.of("rating").descending()) - * .limit(10); - * ``` - * - * @param limit The maximum number of documents to return. - * @return A new Pipeline object with this stage appended to the stage list. - */ - limit(limit: number): Pipeline { - const copy = this.stages.map(s => s); - copy.push(new Limit(limit)); - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy, - this.converter - ); - } - - /** - * Returns a set of distinct {@link Expr} values from the inputs to this stage. - * - *

    This stage run through the results from previous stages to include only results with unique - * combinations of {@link Expr} values ({@link Field}, {@link Function}, etc). - * - *

    The parameters to this stage are defined using {@link Selectable} expressions or {@code string}s: - * - *

      - *
    • {@code string}: Name of an existing field
    • - *
    • {@link Field}: References an existing document field.
    • - *
    • {@link Function}: Represents the result of a function with an assigned alias name using - * {@link Expr#as}
    • - *
    - * - *

    Example: - * - * ```typescript - * // Get a list of unique author names in uppercase and genre combinations. - * firestore.pipeline().collection("books") - * .distinct(toUppercase(Field.of("author")).as("authorName"), Field.of("genre"), "publishedAt") - * .select("authorName"); - * ``` - * - * @param selectables The {@link Selectable} expressions to consider when determining distinct - * value combinations or {@code string}s representing field names. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - distinct(...groups: Array): Pipeline { - const copy = this.stages.map(s => s); - copy.push( - new Distinct( - this.readUserData('distinct', this.selectablesToMap(groups || [])) - ) - ); - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy, - this.converter - ); - } - - /** - * Performs aggregation operations on the documents from previous stages. - * - *

    This stage allows you to calculate aggregate values over a set of documents. You define the - * aggregations to perform using {@link AccumulatorTarget} expressions which are typically results of - * calling {@link Expr#as} on {@link Accumulator} instances. - * - *

    Example: - * - * ```typescript - * // Calculate the average rating and the total number of books - * firestore.pipeline().collection("books") - * .aggregate( - * Field.of("rating").avg().as("averageRating"), - * countAll().as("totalBooks") - * ); - * ``` - * - * @param accumulators The {@link AccumulatorTarget} expressions, each wrapping an {@link Accumulator} - * and provide a name for the accumulated results. - * @return A new Pipeline object with this stage appended to the stage list. - */ - aggregate(...accumulators: AccumulatorTarget[]): Pipeline; - /** - * Performs optionally grouped aggregation operations on the documents from previous stages. - * - *

    This stage allows you to calculate aggregate values over a set of documents, optionally - * grouped by one or more fields or functions. You can specify: - * - *

      - *
    • **Grouping Fields or Functions:** One or more fields or functions to group the documents - * by. For each distinct combination of values in these fields, a separate group is created. - * If no grouping fields are provided, a single group containing all documents is used. Not - * specifying groups is the same as putting the entire inputs into one group.
    • - *
    • **Accumulators:** One or more accumulation operations to perform within each group. These - * are defined using {@link AccumulatorTarget} expressions, which are typically created by - * calling {@link Expr#as} on {@link Accumulator} instances. Each aggregation - * calculates a value (e.g., sum, average, count) based on the documents within its group.
    • - *
    - * - *

    Example: - * - * ```typescript - * // Calculate the average rating for each genre. - * firestore.pipeline().collection("books") - * .aggregate({ - * accumulators: [avg(Field.of("rating")).as("avg_rating")] - * groups: ["genre"] - * }); - * ``` - * - * @param aggregate An {@link Aggregate} object that specifies the grouping fields (if any) and - * the aggregation operations to perform. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - aggregate(options: { - accumulators: AccumulatorTarget[]; - groups?: Array; - }): Pipeline; - aggregate( - optionsOrTarget: - | AccumulatorTarget - | { - accumulators: AccumulatorTarget[]; - groups?: Array; - }, - ...rest: AccumulatorTarget[] - ): Pipeline { - const copy = this.stages.map(s => s); - if ('accumulators' in optionsOrTarget) { - copy.push( - new Aggregate( - new Map( - optionsOrTarget.accumulators.map((target: AccumulatorTarget) => [ - (target as unknown as AccumulatorTarget).alias, - this.readUserData( - 'aggregate', - (target as unknown as AccumulatorTarget).expr - ) - ]) - ), - this.readUserData( - 'aggregate', - this.selectablesToMap(optionsOrTarget.groups || []) - ) - ) - ); - } else { - copy.push( - new Aggregate( - new Map( - [optionsOrTarget, ...rest].map(target => [ - (target as unknown as AccumulatorTarget).alias, - this.readUserData( - 'aggregate', - (target as unknown as AccumulatorTarget).expr - ) - ]) - ), - new Map() - ) - ); - } - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy, - this.converter - ); - } - - findNearest(options: FindNearestOptions): Pipeline; - findNearest(options: FindNearestOptions): Pipeline { - const copy = this.stages.map(s => s); - const parseContext = this.userDataReader.createContext( - UserDataSource.Argument, - 'findNearest' - ); - const value = parseVectorValue(options.vectorValue, parseContext); - const vectorObjectValue = new ObjectValue(value); - copy.push( - new FindNearest( - options.field, - vectorObjectValue, - options.distanceMeasure, - options.limit, - options.distanceField - ) - ); - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy - ); - } - - /** - * Sorts the documents from previous stages based on one or more {@link Ordering} criteria. - * - *

    This stage allows you to order the results of your pipeline. You can specify multiple {@link - * Ordering} instances to sort by multiple fields in ascending or descending order. If documents - * have the same value for a field used for sorting, the next specified ordering will be used. If - * all orderings result in equal comparison, the documents are considered equal and the order is - * unspecified. - * - *

    Example: - * - * ```typescript - * // Sort books by rating in descending order, and then by title in ascending order for books - * // with the same rating - * firestore.pipeline().collection("books") - * .sort( - * Ordering.of(Field.of("rating")).descending(), - * Ordering.of(Field.of("title")) // Ascending order is the default - * ); - * ``` - * - * @param orders One or more {@link Ordering} instances specifying the sorting criteria. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - sort(...orderings: Ordering[]): Pipeline; - sort(options: { orderings: Ordering[] }): Pipeline; - sort( - optionsOrOrderings: - | Ordering - | { - orderings: Ordering[]; - }, - ...rest: Ordering[] - ): Pipeline { - const copy = this.stages.map(s => s); - // Option object - if ('orderings' in optionsOrOrderings) { - copy.push( - new Sort( - this.readUserData( - 'sort', - this.readUserData('sort', optionsOrOrderings.orderings) - ) - ) - ); - } else { - // Ordering object - copy.push( - new Sort(this.readUserData('sort', [optionsOrOrderings, ...rest])) - ); - } - - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy, - this.converter - ); - } - - /** - * Adds a generic stage to the pipeline. - * - *

    This method provides a flexible way to extend the pipeline's functionality by adding custom - * stages. Each generic stage is defined by a unique `name` and a set of `params` that control its - * behavior. - * - *

    Example (Assuming there is no "where" stage available in SDK): - * - * ```typescript - * // Assume we don't have a built-in "where" stage - * firestore.pipeline().collection("books") - * .genericStage("where", [Field.of("published").lt(1900)]) // Custom "where" stage - * .select("title", "author"); - * ``` - * - * @param name The unique name of the generic stage to add. - * @param params A list of parameters to configure the generic stage's behavior. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - genericStage(name: string, params: any[]): Pipeline { - const copy = this.stages.map(s => s); - params.forEach(param => { - if (isReadableUserData(param)) { - param._readUserData(this.userDataReader); - } - }); - copy.push(new GenericStage(name, params)); - return new Pipeline( - this._db, - this.userDataReader, - this.userDataWriter, - this.documentReferenceFactory, - copy, - this.converter - ); - } - - // TODO(pipeline) support converter - // withConverter(converter: null): Pipeline; - // withConverter( - // converter: FirestorePipelineConverter - // ): Pipeline; - // /** - // * Applies a custom data converter to this Query, allowing you to use your - // * own custom model objects with Firestore. When you call get() on the - // * returned Query, the provided converter will convert between Firestore - // * data of type `NewDbModelType` and your custom type `NewAppModelType`. - // * - // * Using the converter allows you to specify generic type arguments when - // * storing and retrieving objects from Firestore. - // * - // * Passing in `null` as the converter parameter removes the current - // * converter. - // * - // * @example - // * ``` - // * class Post { - // * constructor(readonly title: string, readonly author: string) {} - // * - // * toString(): string { - // * return this.title + ', by ' + this.author; - // * } - // * } - // * - // * const postConverter = { - // * toFirestore(post: Post): FirebaseFirestore.DocumentData { - // * return {title: post.title, author: post.author}; - // * }, - // * fromFirestore( - // * snapshot: FirebaseFirestore.QueryDocumentSnapshot - // * ): Post { - // * const data = snapshot.data(); - // * return new Post(data.title, data.author); - // * } - // * }; - // * - // * const postSnap = await Firestore() - // * .collection('posts') - // * .withConverter(postConverter) - // * .doc().get(); - // * const post = postSnap.data(); - // * if (post !== undefined) { - // * post.title; // string - // * post.toString(); // Should be defined - // * post.someNonExistentProperty; // TS error - // * } - // * - // * ``` - // * @param {FirestoreDataConverter | null} converter Converts objects to and - // * from Firestore. Passing in `null` removes the current converter. - // * @return A Query that uses the provided converter. - // */ - // withConverter( - // converter: FirestorePipelineConverter | null - // ): Pipeline { - // const copy = this.stages.map(s => s); - // return new Pipeline( - // this.db, - // copy, - // converter ?? defaultPipelineConverter() - // ); - // } - - /** - * Executes this pipeline and returns a Promise to represent the asynchronous operation. - * - *

    The returned Promise can be used to track the progress of the pipeline execution - * and retrieve the results (or handle any errors) asynchronously. - * - *

    The pipeline results are returned as a list of {@link PipelineResult} objects. Each {@link - * PipelineResult} typically represents a single key/value map that has passed through all the - * stages of the pipeline, however this might differ depending on the stages involved in the - * pipeline. For example: - * - *

      - *
    • If there are no stages or only transformation stages, each {@link PipelineResult} - * represents a single document.
    • - *
    • If there is an aggregation, only a single {@link PipelineResult} is returned, - * representing the aggregated results over the entire dataset .
    • - *
    • If there is an aggregation stage with grouping, each {@link PipelineResult} represents a - * distinct group and its associated aggregated values.
    • - *
    - * - *

    Example: - * - * ```typescript - * const futureResults = await firestore.pipeline().collection("books") - * .where(gt(Field.of("rating"), 4.5)) - * .select("title", "author", "rating") - * .execute(); - * ``` - * - * @return A Promise representing the asynchronous pipeline execution. - */ - execute(): Promise>> { - const datastore = getDatastore(this._db); - return invokeExecutePipeline(datastore, this).then(result => { - const docs = result - // Currently ignore any response from ExecutePipeline that does - // not contain any document data in the `fields` property. - .filter(element => !!element.fields) - .map( - element => - new PipelineResult( - this.userDataWriter, - element.key?.path - ? this.documentReferenceFactory(element.key) - : undefined, - element.fields, - element.executionTime?.toTimestamp(), - element.createTime?.toTimestamp(), - element.updateTime?.toTimestamp() - //this.converter - ) - ); - - return docs; - }); - } - - /** - * @internal - * @private - */ - _toProto(jsonProtoSerializer: JsonProtoSerializer): ExecutePipelineRequest { - const stages: ProtoStage[] = this.stages.map(stage => - stage._toProto(jsonProtoSerializer) - ); - const structuredPipeline: StructuredPipeline = { pipeline: { stages } }; - return { - database: getEncodedDatabaseId(jsonProtoSerializer), - structuredPipeline - }; - } -} - -/** - * Experimental Modular API for console testing. - * @param firestore - */ -export function pipeline(firestore: Firestore): PipelineSource; - -/** - * Experimental Modular API for console testing. - * @param query - */ -export function pipeline(query: Query): Pipeline; - -export function pipeline( - firestoreOrQuery: Firestore | Query -): PipelineSource | Pipeline { - return firestoreOrQuery.pipeline(); -} diff --git a/packages/firestore/src/lite-api/pipeline_impl.ts b/packages/firestore/src/lite-api/pipeline_impl.ts deleted file mode 100644 index cd490d31871..00000000000 --- a/packages/firestore/src/lite-api/pipeline_impl.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Pipeline } from './pipeline'; -import { PipelineResult } from './pipeline-result'; - -/** - * Modular API for console experimentation. - * @param pipeline Execute this pipeline. - * @beta - */ -export function execute( - pipeline: Pipeline -): Promise>> { - return pipeline.execute(); -} diff --git a/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index 8319761766d..f38dad9a078 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -40,7 +40,6 @@ import { AutoId } from '../util/misc'; import { Firestore } from './database'; import { FieldPath } from './field_path'; import { FieldValue } from './field_value'; -import type { Pipeline } from './pipeline'; import { FirestoreDataConverter } from './snapshot'; import { NestedUpdateFields, Primitive } from './types'; @@ -181,15 +180,6 @@ export class Query< this._query ); } - - /** - * Pipeline query. - */ - pipeline(): Pipeline { - throw new Error( - 'Pipelines not initialized. Your application must call `useFirestorePipelines()` before using Firestore Pipeline features.' - ); - } } /** diff --git a/packages/firestore/src/lite-api/stage.ts b/packages/firestore/src/lite-api/stage.ts deleted file mode 100644 index b4fe237f79c..00000000000 --- a/packages/firestore/src/lite-api/stage.ts +++ /dev/null @@ -1,380 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ObjectValue } from '../model/object_value'; -import { - Stage as ProtoStage, - Value as ProtoValue -} from '../protos/firestore_proto_api'; -import { toNumber } from '../remote/number_serializer'; -import { - JsonProtoSerializer, - ProtoSerializable, - toMapValue, - toStringValue -} from '../remote/serializer'; - -import { - Accumulator, - Expr, - Field, - FilterCondition, - Ordering -} from './expressions'; -import { DocumentReference } from './reference'; -import { VectorValue } from './vector_value'; - -/** - * @beta - */ -export interface Stage extends ProtoSerializable { - name: string; -} - -/** - * @beta - */ -export class AddFields implements Stage { - name = 'add_fields'; - - constructor(private fields: Map) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: [toMapValue(serializer, this.fields)] - }; - } -} - -/** - * @beta - */ -export class Aggregate implements Stage { - name = 'aggregate'; - - constructor( - private accumulators: Map, - private groups: Map - ) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: [ - toMapValue(serializer, this.accumulators), - toMapValue(serializer, this.groups) - ] - }; - } -} - -/** - * @beta - */ -export class Distinct implements Stage { - name = 'distinct'; - - constructor(private groups: Map) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: [toMapValue(serializer, this.groups)] - }; - } -} - -/** - * @beta - */ -export class CollectionSource implements Stage { - name = 'collection'; - - constructor(private collectionPath: string) { - if (!this.collectionPath.startsWith('/')) { - this.collectionPath = '/' + this.collectionPath; - } - } - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: [{ referenceValue: this.collectionPath }] - }; - } -} - -/** - * @beta - */ -export class CollectionGroupSource implements Stage { - name = 'collection_group'; - - constructor(private collectionId: string) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: [{ referenceValue: '' }, { stringValue: this.collectionId }] - }; - } -} - -/** - * @beta - */ -export class DatabaseSource implements Stage { - name = 'database'; - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name - }; - } -} - -/** - * @beta - */ -export class DocumentsSource implements Stage { - name = 'documents'; - - constructor(private docPaths: string[]) {} - - static of(refs: DocumentReference[]): DocumentsSource { - return new DocumentsSource(refs.map(ref => '/' + ref.path)); - } - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: this.docPaths.map(p => { - return { referenceValue: p }; - }) - }; - } -} - -/** - * @beta - */ -export class Where implements Stage { - name = 'where'; - - constructor(private condition: FilterCondition & Expr) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: [(this.condition as unknown as Expr)._toProto(serializer)] - }; - } -} - -/** - * @beta - */ -export interface FindNearestOptions { - field: Field; - vectorValue: VectorValue | number[]; - distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; - limit?: number; - distanceField?: string; -} - -/** - * @beta - */ -export class FindNearest implements Stage { - name = 'find_nearest'; - - /** - * @private - * @internal - * - * @param _field - * @param _vectorValue - * @param _distanceMeasure - * @param _limit - * @param _distanceField - */ - constructor( - private _field: Field, - private _vectorValue: ObjectValue, - private _distanceMeasure: 'euclidean' | 'cosine' | 'dot_product', - private _limit?: number, - private _distanceField?: string - ) {} - - /** - * @private - * @internal - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - const options: { [k: string]: ProtoValue } = {}; - - if (this._limit) { - options.limit = toNumber(serializer, this._limit)!; - } - - if (this._distanceField) { - // eslint-disable-next-line camelcase - options.distance_field = Field.of(this._distanceField)._toProto( - serializer - ); - } - - return { - name: this.name, - args: [ - this._field._toProto(serializer), - this._vectorValue.value, - toStringValue(this._distanceMeasure) - ], - options - }; - } -} - -/** - * @beta - */ -export class Limit implements Stage { - name = 'limit'; - - constructor(private limit: number) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: [toNumber(serializer, this.limit)] - }; - } -} - -/** - * @beta - */ -export class Offset implements Stage { - name = 'offset'; - - constructor(private offset: number) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: [toNumber(serializer, this.offset)] - }; - } -} - -/** - * @beta - */ -export class Select implements Stage { - name = 'select'; - - constructor(private projections: Map) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: [toMapValue(serializer, this.projections)] - }; - } -} - -/** - * @beta - */ -export class Sort implements Stage { - name = 'sort'; - - constructor(private orders: Ordering[]) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - return { - name: this.name, - args: this.orders.map(o => o._toProto(serializer)) - }; - } -} - -/** - * @beta - */ -export class GenericStage implements Stage { - constructor(public name: string, params: unknown[]) {} - - /** - * @internal - * @private - */ - _toProto(serializer: JsonProtoSerializer): ProtoStage { - // TODO support generic stage - return {}; - } -} diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index 2179d7854b3..a3022be627e 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -854,7 +854,7 @@ function parseSentinelFieldValue( * * @returns The parsed value */ -export function parseScalarValue( +function parseScalarValue( value: unknown, context: ParseContextImpl ): ProtoValue | null { @@ -922,10 +922,9 @@ export function parseScalarValue( * Creates a new VectorValue proto value (using the internal format). */ export function parseVectorValue( - value: VectorValue | number[], + value: VectorValue, context: ParseContextImpl -): { mapValue: ProtoMapValue } { - const values = value instanceof VectorValue ? value.toArray() : value; +): ProtoValue { const mapValue: ProtoMapValue = { fields: { [TYPE_KEY]: { @@ -933,7 +932,7 @@ export function parseVectorValue( }, [VECTOR_MAP_VECTORS_KEY]: { arrayValue: { - values: values.map(value => { + values: value.toArray().map(value => { if (typeof value !== 'number') { throw context.createError( 'VectorValues must only contain numeric values.' diff --git a/packages/firestore/src/local/indexeddb_persistence.ts b/packages/firestore/src/local/indexeddb_persistence.ts index 0ec2baabfe4..8db4ac25d03 100644 --- a/packages/firestore/src/local/indexeddb_persistence.ts +++ b/packages/firestore/src/local/indexeddb_persistence.ts @@ -58,11 +58,7 @@ import { IndexedDbTargetCache } from './indexeddb_target_cache'; import { getStore, IndexedDbTransaction } from './indexeddb_transaction'; import { LocalSerializer } from './local_serializer'; import { LruParams } from './lru_garbage_collector'; -import { - DatabaseDeletedListener, - Persistence, - PrimaryStateListener -} from './persistence'; +import { Persistence, PrimaryStateListener } from './persistence'; import { PersistencePromise } from './persistence_promise'; import { PersistenceTransaction, @@ -131,11 +127,11 @@ export const MAIN_DATABASE = 'main'; * `enablePersistence()` with `{synchronizeTabs:true}`. * * In multi-tab mode, if multiple clients are active at the same time, the SDK - * will designate one client as the “primary client”. An effort is made to pick + * will designate one client as the "primary client". An effort is made to pick * a visible, network-connected and active client, and this client is * responsible for letting other clients know about its presence. The primary * client writes a unique client-generated identifier (the client ID) to - * IndexedDb’s “owner” store every 4 seconds. If the primary client fails to + * IndexedDb’s "owner" store every 4 seconds. If the primary client fails to * update this entry, another client can acquire the lease and take over as * primary. * @@ -328,25 +324,20 @@ export class IndexedDbPersistence implements Persistence { } /** - * Registers a listener that gets called when the underlying database receives - * an event indicating that it either has been deleted or is pending deletion - * and must be closed. - * - * For example, this callback will be called in the case that multi-tab - * IndexedDB persistence is in use and another tab calls - * clearIndexedDbPersistence(). In that case, this Firestore instance must - * close its IndexedDB connection in order to allow the deletion initiated by - * the other tab to proceed. - * - * This method may only be called once; subsequent invocations will result in - * an exception, refusing to supersede the previously-registered listener. + * Registers a listener that gets called when the database receives a + * version change event indicating that it has deleted. * * PORTING NOTE: This is only used for Web multi-tab. */ setDatabaseDeletedListener( - databaseDeletedListener: DatabaseDeletedListener + databaseDeletedListener: () => Promise ): void { - this.simpleDb.setDatabaseDeletedListener(databaseDeletedListener); + this.simpleDb.setVersionChangeListener(async event => { + // Check if an attempt is made to delete IndexedDB. + if (event.newVersion === null) { + await databaseDeletedListener(); + } + }); } /** diff --git a/packages/firestore/src/local/persistence.ts b/packages/firestore/src/local/persistence.ts index 113efe7b7d3..b014a6479ac 100644 --- a/packages/firestore/src/local/persistence.ts +++ b/packages/firestore/src/local/persistence.ts @@ -98,8 +98,6 @@ export interface ReferenceDelegate { ): PersistencePromise; } -export type DatabaseDeletedListener = () => void; - /** * Persistence is the lowest-level shared interface to persistent storage in * Firestore. @@ -153,23 +151,13 @@ export interface Persistence { shutdown(): Promise; /** - * Registers a listener that gets called when the underlying database receives - * an event indicating that it either has been deleted or is pending deletion - * and must be closed. - * - * For example, this callback will be called in the case that multi-tab - * IndexedDB persistence is in use and another tab calls - * clearIndexedDbPersistence(). In that case, this Firestore instance must - * close its IndexedDB connection in order to allow the deletion initiated by - * the other tab to proceed. - * - * This method may only be called once; subsequent invocations will result in - * an exception, refusing to supersede the previously-registered listener. + * Registers a listener that gets called when the database receives a + * version change event indicating that it has deleted. * * PORTING NOTE: This is only used for Web multi-tab. */ setDatabaseDeletedListener( - databaseDeletedListener: DatabaseDeletedListener + databaseDeletedListener: () => Promise ): void; /** diff --git a/packages/firestore/src/local/simple_db.ts b/packages/firestore/src/local/simple_db.ts index 1e315c5dae6..1958d853690 100644 --- a/packages/firestore/src/local/simple_db.ts +++ b/packages/firestore/src/local/simple_db.ts @@ -19,10 +19,9 @@ import { getGlobal, getUA, isIndexedDBAvailable } from '@firebase/util'; import { debugAssert } from '../util/assert'; import { Code, FirestoreError } from '../util/error'; -import { logDebug, logError, logWarn } from '../util/log'; +import { logDebug, logError } from '../util/log'; import { Deferred } from '../util/promise'; -import { DatabaseDeletedListener } from './persistence'; import { PersistencePromise } from './persistence_promise'; // References to `indexedDB` are guarded by SimpleDb.isAvailable() and getGlobal() @@ -159,8 +158,8 @@ export class SimpleDbTransaction { */ export class SimpleDb { private db?: IDBDatabase; - private databaseDeletedListener?: DatabaseDeletedListener; private lastClosedDbVersion: number | null = null; + private versionchangelistener?: (event: IDBVersionChangeEvent) => void; /** Deletes the specified database. */ static delete(name: string): Promise { @@ -349,24 +348,6 @@ export class SimpleDb { event.oldVersion ); const db = (event.target as IDBOpenDBRequest).result; - if ( - this.lastClosedDbVersion !== null && - this.lastClosedDbVersion !== event.oldVersion - ) { - // This thrown error will get passed to the `onerror` callback - // registered above, and will then be propagated correctly. - throw new Error( - `refusing to open IndexedDB database due to potential ` + - `corruption of the IndexedDB database data; this corruption ` + - `could be caused by clicking the "clear site data" button in ` + - `a web browser; try reloading the web page to re-initialize ` + - `the IndexedDB database: ` + - `lastClosedDbVersion=${this.lastClosedDbVersion}, ` + - `event.oldVersion=${event.oldVersion}, ` + - `event.newVersion=${event.newVersion}, ` + - `db.version=${db.version}` - ); - } this.schemaConverter .createOrUpgrade( db, @@ -382,46 +363,24 @@ export class SimpleDb { }); }; }); - - this.db.addEventListener( - 'close', - event => { - const db = event.target as IDBDatabase; - this.lastClosedDbVersion = db.version; - }, - { passive: true } - ); } - this.db.addEventListener( - 'versionchange', - event => { - // Notify the listener if another tab attempted to delete the IndexedDb - // database, such as by calling clearIndexedDbPersistence(). - if (event.newVersion === null) { - logWarn( - `Received "versionchange" event with newVersion===null; ` + - 'notifying the registered DatabaseDeletedListener, if any' - ); - this.databaseDeletedListener?.(); - } - }, - { passive: true } - ); + if (this.versionchangelistener) { + this.db.onversionchange = event => this.versionchangelistener!(event); + } return this.db; } - setDatabaseDeletedListener( - databaseDeletedListener: DatabaseDeletedListener + setVersionChangeListener( + versionChangeListener: (event: IDBVersionChangeEvent) => void ): void { - if (this.databaseDeletedListener) { - throw new Error( - 'setDatabaseDeletedListener() may only be called once, ' + - 'and it has already been called' - ); + this.versionchangelistener = versionChangeListener; + if (this.db) { + this.db.onversionchange = (event: IDBVersionChangeEvent) => { + return versionChangeListener(event); + }; } - this.databaseDeletedListener = databaseDeletedListener; } async runTransaction( diff --git a/packages/firestore/src/model/aggregate_result_value.ts b/packages/firestore/src/model/aggregate_result_value.ts deleted file mode 100644 index 042dc29d345..00000000000 --- a/packages/firestore/src/model/aggregate_result_value.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - MapValue as ProtoMapValue, - Value as ProtoValue -} from '../protos/firestore_proto_api'; - -import { valueEquals } from './values'; - -/** - * An AggregateResultValue represents a MapValue in the Firestore Proto. - */ -export class AggregateResultValue { - constructor(readonly value: { mapValue: ProtoMapValue }) {} - - static empty(): AggregateResultValue { - return new AggregateResultValue({ mapValue: {} }); - } - - aggregate(alias: string): ProtoValue | null { - return this.value.mapValue.fields?.[alias] ?? null; - } - - isEqual(other: AggregateResultValue): boolean { - return valueEquals(this.value, other.value); - } -} diff --git a/packages/firestore/src/model/pipeline_stream_element.ts b/packages/firestore/src/model/pipeline_stream_element.ts deleted file mode 100644 index efa27e2cc44..00000000000 --- a/packages/firestore/src/model/pipeline_stream_element.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SnapshotVersion } from '../core/snapshot_version'; - -import { DocumentKey } from './document_key'; -import { ObjectValue } from './object_value'; - -export interface PipelineStreamElement { - transaction?: string; - key?: DocumentKey; - executionTime?: SnapshotVersion; - createTime?: SnapshotVersion; - updateTime?: SnapshotVersion; - fields?: ObjectValue; -} diff --git a/packages/firestore/src/protos/compile.sh b/packages/firestore/src/protos/compile.sh index 9f9fb4b3217..26c46d1a40d 100755 --- a/packages/firestore/src/protos/compile.sh +++ b/packages/firestore/src/protos/compile.sh @@ -18,17 +18,10 @@ set -euo pipefail # Variables PROTOS_DIR="." -PBJS="../../node_modules/.bin/pbjs" -PBTS="../../node_modules/.bin/pbts" +PBJS="$(npm bin)/pbjs" -"${PBJS}" --path=. --target=json -o protos.json \ - -r firestore/v1 "${PROTOS_DIR}/google/firestore/v1/*.proto" \ +"${PBJS}" --proto_path=. --target=json -o protos.json \ + -r firestore_v1 \ + "${PROTOS_DIR}/google/firestore/v1/*.proto" \ "${PROTOS_DIR}/google/protobuf/*.proto" "${PROTOS_DIR}/google/type/*.proto" \ "${PROTOS_DIR}/google/rpc/*.proto" "${PROTOS_DIR}/google/api/*.proto" - -"${PBJS}" --path="${PROTOS_DIR}" --target=static -o temp.js \ - -r firestore/v1 "${PROTOS_DIR}/google/firestore/v1/*.proto" \ - "${PROTOS_DIR}/google/protobuf/*.proto" "${PROTOS_DIR}/google/type/*.proto" \ - "${PROTOS_DIR}/google/rpc/*.proto" "${PROTOS_DIR}/google/api/*.proto" - -"${PBTS}" -o temp.d.ts --no-comments temp.js diff --git a/packages/firestore/src/protos/firestore_proto_api.ts b/packages/firestore/src/protos/firestore_proto_api.ts index cc1c57259f5..9618d71b86a 100644 --- a/packages/firestore/src/protos/firestore_proto_api.ts +++ b/packages/firestore/src/protos/firestore_proto_api.ts @@ -145,21 +145,9 @@ export interface IValueNullValueEnum { } export declare const ValueNullValueEnum: IValueNullValueEnum; export declare namespace firestoreV1ApiClientInterfaces { - interface Aggregation { - count?: Count; - sum?: Sum; - avg?: Avg; - alias?: string; - } - interface AggregationResult { - aggregateFields?: ApiClientObjectMap; - } interface ArrayValue { values?: Value[]; } - interface Avg { - field?: FieldReference; - } interface BatchGetDocumentsRequest { database?: string; documents?: string[]; @@ -180,14 +168,6 @@ export declare namespace firestoreV1ApiClientInterfaces { interface BeginTransactionResponse { transaction?: string; } - interface BitSequence { - bitmap?: string | Uint8Array; - padding?: number; - } - interface BloomFilter { - bits?: BitSequence; - hashCount?: number; - } interface CollectionSelector { collectionId?: string; allDescendants?: boolean; @@ -205,9 +185,6 @@ export declare namespace firestoreV1ApiClientInterfaces { op?: CompositeFilterOp; filters?: Filter[]; } - interface Count { - upTo?: number; - } interface Cursor { values?: Value[]; before?: boolean; @@ -244,23 +221,19 @@ export declare namespace firestoreV1ApiClientInterfaces { documents?: string[]; } interface Empty {} - interface ExecutePipelineRequest { - database?: string; - structuredPipeline?: StructuredPipeline; - transaction?: string; - newTransaction?: TransactionOptions; - readTime?: string; - } - interface ExecutePipelineResponse { - transaction?: string; - results?: Document[]; - executionTime?: string; - } interface ExistenceFilter { targetId?: number; count?: number; unchangedNames?: BloomFilter; } + interface BloomFilter { + bits?: BitSequence; + hashCount?: number; + } + interface BitSequence { + bitmap?: string | Uint8Array; + padding?: number; + } interface FieldFilter { field?: FieldReference; op?: FieldFilterOp; @@ -281,11 +254,6 @@ export declare namespace firestoreV1ApiClientInterfaces { fieldFilter?: FieldFilter; unaryFilter?: UnaryFilter; } - interface Function { - name?: string; - args?: Value[]; - options?: ApiClientObjectMap; - } interface Index { name?: string; collectionId?: string; @@ -342,9 +310,6 @@ export declare namespace firestoreV1ApiClientInterfaces { field?: FieldReference; direction?: OrderDirection; } - interface Pipeline { - stages?: Stage[]; - } interface Precondition { exists?: boolean; updateTime?: Timestamp; @@ -390,24 +355,33 @@ export declare namespace firestoreV1ApiClientInterfaces { transaction?: string; readTime?: string; } + interface AggregationResult { + aggregateFields?: ApiClientObjectMap; + } interface StructuredAggregationQuery { structuredQuery?: StructuredQuery; aggregations?: Aggregation[]; } - interface Stage { - name?: string; - args?: Value[]; - options?: ApiClientObjectMap; + interface Aggregation { + count?: Count; + sum?: Sum; + avg?: Avg; + alias?: string; + } + interface Count { + upTo?: number; + } + interface Sum { + field?: FieldReference; + } + interface Avg { + field?: FieldReference; } interface Status { code?: number; message?: string; details?: Array>; } - interface StructuredPipeline { - pipeline?: Pipeline; - options?: ApiClientObjectMap; - } interface StructuredQuery { select?: Projection; from?: CollectionSelector[]; @@ -418,9 +392,6 @@ export declare namespace firestoreV1ApiClientInterfaces { offset?: number; limit?: number | { value: number }; } - interface Sum { - field?: FieldReference; - } interface Target { query?: QueryTarget; documents?: DocumentsTarget; @@ -457,10 +428,6 @@ export declare namespace firestoreV1ApiClientInterfaces { geoPointValue?: LatLng; arrayValue?: ArrayValue; mapValue?: MapValue; - fieldReferenceValue?: string; - // eslint-disable-next-line @typescript-eslint/ban-types - functionValue?: Function; - pipelineValue?: Pipeline; } interface Write { update?: Document; @@ -522,17 +489,12 @@ export declare type DocumentsTarget = export declare type Empty = firestoreV1ApiClientInterfaces.Empty; export declare type ExistenceFilter = firestoreV1ApiClientInterfaces.ExistenceFilter; -export declare type ExecutePipelineRequest = - firestoreV1ApiClientInterfaces.ExecutePipelineRequest; -export declare type ExecutePipelineResponse = - firestoreV1ApiClientInterfaces.ExecutePipelineResponse; export declare type FieldFilter = firestoreV1ApiClientInterfaces.FieldFilter; export declare type FieldReference = firestoreV1ApiClientInterfaces.FieldReference; export declare type FieldTransform = firestoreV1ApiClientInterfaces.FieldTransform; export declare type Filter = firestoreV1ApiClientInterfaces.Filter; -export declare type Function = firestoreV1ApiClientInterfaces.Function; export declare type Index = firestoreV1ApiClientInterfaces.Index; export declare type IndexField = firestoreV1ApiClientInterfaces.IndexField; export declare type LatLng = firestoreV1ApiClientInterfaces.LatLng; @@ -551,7 +513,6 @@ export declare type ListenResponse = export declare type MapValue = firestoreV1ApiClientInterfaces.MapValue; export declare type Operation = firestoreV1ApiClientInterfaces.Operation; export declare type Order = firestoreV1ApiClientInterfaces.Order; -export declare type Pipeline = firestoreV1ApiClientInterfaces.Pipeline; export declare type Precondition = firestoreV1ApiClientInterfaces.Precondition; export declare type Projection = firestoreV1ApiClientInterfaces.Projection; export declare type QueryTarget = firestoreV1ApiClientInterfaces.QueryTarget; @@ -568,12 +529,9 @@ export declare type RunAggregationQueryRequest = export declare type Aggregation = firestoreV1ApiClientInterfaces.Aggregation; export declare type RunAggregationQueryResponse = firestoreV1ApiClientInterfaces.RunAggregationQueryResponse; -export declare type Stage = firestoreV1ApiClientInterfaces.Stage; export declare type Status = firestoreV1ApiClientInterfaces.Status; export declare type StructuredQuery = firestoreV1ApiClientInterfaces.StructuredQuery; -export declare type StructuredPipeline = - firestoreV1ApiClientInterfaces.StructuredPipeline; export declare type Target = firestoreV1ApiClientInterfaces.Target; export declare type TargetChange = firestoreV1ApiClientInterfaces.TargetChange; export declare type TransactionOptions = diff --git a/packages/firestore/src/protos/google/api/launch_stage.proto b/packages/firestore/src/protos/google/api/launch_stage.proto deleted file mode 100644 index 9863fc23d42..00000000000 --- a/packages/firestore/src/protos/google/api/launch_stage.proto +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api;api"; -option java_multiple_files = true; -option java_outer_classname = "LaunchStageProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// The launch stage as defined by [Google Cloud Platform -// Launch Stages](https://cloud.google.com/terms/launch-stages). -enum LaunchStage { - // Do not use this default value. - LAUNCH_STAGE_UNSPECIFIED = 0; - - // The feature is not yet implemented. Users can not use it. - UNIMPLEMENTED = 6; - - // Prelaunch features are hidden from users and are only visible internally. - PRELAUNCH = 7; - - // Early Access features are limited to a closed group of testers. To use - // these features, you must sign up in advance and sign a Trusted Tester - // agreement (which includes confidentiality provisions). These features may - // be unstable, changed in backward-incompatible ways, and are not - // guaranteed to be released. - EARLY_ACCESS = 1; - - // Alpha is a limited availability test for releases before they are cleared - // for widespread use. By Alpha, all significant design issues are resolved - // and we are in the process of verifying functionality. Alpha customers - // need to apply for access, agree to applicable terms, and have their - // projects allowlisted. Alpha releases don't have to be feature complete, - // no SLAs are provided, and there are no technical support obligations, but - // they will be far enough along that customers can actually use them in - // test environments or for limited-use tests -- just like they would in - // normal production cases. - ALPHA = 2; - - // Beta is the point at which we are ready to open a release for any - // customer to use. There are no SLA or technical support obligations in a - // Beta release. Products will be complete from a feature perspective, but - // may have some open outstanding issues. Beta releases are suitable for - // limited production use cases. - BETA = 3; - - // GA features are open to all developers and are considered stable and - // fully qualified for production use. - GA = 4; - - // Deprecated features are scheduled to be shut down and removed. For more - // information, see the "Deprecation Policy" section of our [Terms of - // Service](https://cloud.google.com/terms/) - // and the [Google Cloud Platform Subject to the Deprecation - // Policy](https://cloud.google.com/terms/deprecation) documentation. - DEPRECATED = 5; -} diff --git a/packages/firestore/src/protos/google/firestore/v1/document.proto b/packages/firestore/src/protos/google/firestore/v1/document.proto index f7605750502..5238a943ce4 100644 --- a/packages/firestore/src/protos/google/firestore/v1/document.proto +++ b/packages/firestore/src/protos/google/firestore/v1/document.proto @@ -129,48 +129,6 @@ message Value { // A map value. MapValue map_value = 6; - // Value which references a field. - // - // This is considered relative (vs absolute) since it only refers to a field - // and not a field within a particular document. - // - // **Requires:** - // - // * Must follow [field reference][FieldReference.field_path] limitations. - // - // * Not allowed to be used when writing documents. - // - // (-- NOTE(batchik): long term, there is no reason this type should not be - // allowed to be used on the write path. --) - string field_reference_value = 19; - - // A value that represents an unevaluated expression. - // - // **Requires:** - // - // * Not allowed to be used when writing documents. - // - // (-- NOTE(batchik): similar to above, there is no reason to not allow - // storing expressions into the database, just no plan to support in - // the near term. - // - // This would actually be an interesting way to represent user-defined - // functions or more expressive rules-based systems. --) - Function function_value = 20; - - // A value that represents an unevaluated pipeline. - // - // **Requires:** - // - // * Not allowed to be used when writing documents. - // - // (-- NOTE(batchik): similar to above, there is no reason to not allow - // storing expressions into the database, just no plan to support in - // the near term. - // - // This would actually be an interesting way to represent user-defined - // functions or more expressive rules-based systems. --) - Pipeline pipeline_value = 21; } } @@ -190,73 +148,3 @@ message MapValue { // not exceed 1,500 bytes and cannot be empty. map fields = 1; } - - -// Represents an unevaluated scalar expression. -// -// For example, the expression `like(user_name, "%alice%")` is represented as: -// -// ``` -// name: "like" -// args { field_reference: "user_name" } -// args { string_value: "%alice%" } -// ``` -// -// (-- api-linter: core::0123::resource-annotation=disabled -// aip.dev/not-precedent: this is not a One Platform API resource. --) -message Function { - // The name of the function to evaluate. - // - // **Requires:** - // - // * must be in snake case (lower case with underscore separator). - // - string name = 1; - - // Ordered list of arguments the given function expects. - repeated Value args = 2; - - // Optional named arguments that certain functions may support. - map options = 3; -} - -// A Firestore query represented as an ordered list of operations / stages. -message Pipeline { - // A single operation within a pipeline. - // - // A stage is made up of a unique name, and a list of arguments. The exact - // number of arguments & types is dependent on the stage type. - // - // To give an example, the stage `filter(state = "MD")` would be encoded as: - // - // ``` - // name: "filter" - // args { - // function_value { - // name: "eq" - // args { field_reference_value: "state" } - // args { string_value: "MD" } - // } - // } - // ``` - // - // See public documentation for the full list. - message Stage { - // The name of the stage to evaluate. - // - // **Requires:** - // - // * must be in snake case (lower case with underscore separator). - // - string name = 1; - - // Ordered list of arguments the given stage expects. - repeated Value args = 2; - - // Optional named arguments that certain functions may support. - map options = 3; - } - - // Ordered list of stages to evaluate. - repeated Stage stages = 1; -} diff --git a/packages/firestore/src/protos/google/firestore/v1/firestore.proto b/packages/firestore/src/protos/google/firestore/v1/firestore.proto index 3e7b62e0609..a8fc0d54b51 100644 --- a/packages/firestore/src/protos/google/firestore/v1/firestore.proto +++ b/packages/firestore/src/protos/google/firestore/v1/firestore.proto @@ -22,7 +22,6 @@ import "google/api/field_behavior.proto"; import "google/firestore/v1/aggregation_result.proto"; import "google/firestore/v1/common.proto"; import "google/firestore/v1/document.proto"; -import "google/firestore/v1/pipeline.proto"; import "google/firestore/v1/query.proto"; import "google/firestore/v1/write.proto"; import "google/protobuf/empty.proto"; @@ -136,15 +135,6 @@ service Firestore { }; } - // Executes a pipeline query. - rpc ExecutePipeline(ExecutePipelineRequest) - returns (stream ExecutePipelineResponse) { - option (google.api.http) = { - post: "/v1/{database=projects/*/databases/*}/documents:executePipeline" - body: "*" - }; - } - // Runs an aggregation query. // // Rather than producing [Document][google.firestore.v1.Document] results like [Firestore.RunQuery][google.firestore.v1.Firestore.RunQuery], @@ -167,7 +157,7 @@ service Firestore { } }; } - + // Partitions a query by returning partition cursors that can be used to run // the query in parallel. The returned partition cursors are split points that // can be used by RunQuery as starting/end points for the query results. @@ -557,82 +547,6 @@ message RunQueryResponse { int32 skipped_results = 4; } -// The request for [Firestore.ExecutePipeline][]. -message ExecutePipelineRequest { - // Database identifier, in the form `projects/{project}/databases/{database}`. - string database = 1 [ - (google.api.field_behavior) = REQUIRED - ]; - - oneof pipeline_type { - // A pipelined operation. - StructuredPipeline structured_pipeline = 2; - } - - // Optional consistency arguments, defaults to strong consistency. - oneof consistency_selector { - // Run the query within an already active transaction. - // - // The value here is the opaque transaction ID to execute the query in. - bytes transaction = 5; - - // Execute the pipeline in a new transaction. - // - // The identifier of the newly created transaction will be returned in the - // first response on the stream. This defaults to a read-only transaction. - TransactionOptions new_transaction = 6; - - // Execute the pipeline in a snapshot transaction at the given time. - // - // This must be a microsecond precision timestamp within the past one hour, - // or if Point-in-Time Recovery is enabled, can additionally be a whole - // minute timestamp within the past 7 days. - google.protobuf.Timestamp read_time = 7; - } - - // Explain / analyze options for the pipeline. - // ExplainOptions explain_options = 8 [(google.api.field_behavior) = OPTIONAL]; -} - -// The response for [Firestore.Execute][]. -message ExecutePipelineResponse { - // Newly created transaction identifier. - // - // This field is only specified on the first response from the server when - // the request specified [ExecuteRequest.new_transaction][]. - bytes transaction = 1; - - // An ordered batch of results returned executing a pipeline. - // - // The batch size is variable, and can even be zero for when only a partial - // progress message is returned. - // - // The fields present in the returned documents are only those that were - // explicitly requested in the pipeline, this include those like - // [`__name__`][Document.name] & [`__update_time__`][Document.update_time]. - // This is explicitly a divergence from `Firestore.RunQuery` / - // `Firestore.GetDocument` RPCs which always return such fields even when they - // are not specified in the [`mask`][DocumentMask]. - repeated Document results = 2; - - // The time at which the document(s) were read. - // - // This may be monotonically increasing; in this case, the previous documents - // in the result stream are guaranteed not to have changed between their - // `execution_time` and this one. - // - // If the query returns no results, a response with `execution_time` and no - // `results` will be sent, and this represents the time at which the operation - // was run. - google.protobuf.Timestamp execution_time = 3; - - // Query explain metrics. - // - // Set on the last response when [ExecutePipelineRequest.explain_options][] - // was specified on the request. - // ExplainMetrics explain_metrics = 4; -} - // The request for [Firestore.RunAggregationQuery][google.firestore.v1.Firestore.RunAggregationQuery]. message RunAggregationQueryRequest { // Required. The parent resource name. In the format: diff --git a/packages/firestore/src/protos/google/firestore/v1/pipeline.proto b/packages/firestore/src/protos/google/firestore/v1/pipeline.proto deleted file mode 100644 index ea5b2309331..00000000000 --- a/packages/firestore/src/protos/google/firestore/v1/pipeline.proto +++ /dev/null @@ -1,41 +0,0 @@ -/*! - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -syntax = "proto3"; -package google.firestore.v1; -import "google/firestore/v1/document.proto"; -option csharp_namespace = "Google.Cloud.Firestore.V1"; -option php_namespace = "Google\\Cloud\\Firestore\\V1"; -option ruby_package = "Google::Cloud::Firestore::V1"; -option java_multiple_files = true; -option java_package = "com.google.firestore.v1"; -option java_outer_classname = "PipelineProto"; -option objc_class_prefix = "GCFS"; -// A Firestore query represented as an ordered list of operations / stages. -// -// This is considered the top-level function which plans & executes a query. -// It is logically equivalent to `query(stages, options)`, but prevents the -// client from having to build a function wrapper. -message StructuredPipeline { - // The pipeline query to execute. - Pipeline pipeline = 1; - // Optional query-level arguments. - // - // (-- Think query statement hints. --) - // - // (-- TODO(batchik): define the api contract of using an unsupported hint --) - map options = 2; -} diff --git a/packages/firestore/src/protos/google/firestore/v1/query_profile.proto b/packages/firestore/src/protos/google/firestore/v1/query_profile.proto deleted file mode 100644 index de27144a038..00000000000 --- a/packages/firestore/src/protos/google/firestore/v1/query_profile.proto +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.firestore.v1; - -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; - -option csharp_namespace = "Google.Cloud.Firestore.V1"; -option go_package = "cloud.google.com/go/firestore/apiv1/firestorepb;firestorepb"; -option java_multiple_files = true; -option java_outer_classname = "QueryProfileProto"; -option java_package = "com.google.firestore.v1"; -option objc_class_prefix = "GCFS"; -option php_namespace = "Google\\Cloud\\Firestore\\V1"; -option ruby_package = "Google::Cloud::Firestore::V1"; - -// Specification of the Firestore Query Profile fields. - -// Explain options for the query. -message ExplainOptions { - // Optional. Whether to execute this query. - // - // When false (the default), the query will be planned, returning only - // metrics from the planning stages. - // - // When true, the query will be planned and executed, returning the full - // query results along with both planning and execution stage metrics. - bool analyze = 1 [(google.api.field_behavior) = OPTIONAL]; -} - -// Explain metrics for the query. -message ExplainMetrics { - // Planning phase information for the query. - PlanSummary plan_summary = 1; - - // Aggregated stats from the execution of the query. Only present when - // [ExplainOptions.analyze][google.firestore.v1.ExplainOptions.analyze] is set - // to true. - ExecutionStats execution_stats = 2; -} - -// Planning phase information for the query. -message PlanSummary { - // The indexes selected for the query. For example: - // [ - // {"query_scope": "Collection", "properties": "(foo ASC, __name__ ASC)"}, - // {"query_scope": "Collection", "properties": "(bar ASC, __name__ ASC)"} - // ] - repeated google.protobuf.Struct indexes_used = 1; -} - -// Execution statistics for the query. -message ExecutionStats { - // Total number of results returned, including documents, projections, - // aggregation results, keys. - int64 results_returned = 1; - - // Total time to execute the query in the backend. - google.protobuf.Duration execution_duration = 3; - - // Total billable read operations. - int64 read_operations = 4; - - // Debugging statistics from the execution of the query. Note that the - // debugging stats are subject to change as Firestore evolves. It could - // include: - // { - // "indexes_entries_scanned": "1000", - // "documents_scanned": "20", - // "billing_details" : { - // "documents_billable": "20", - // "index_entries_billable": "1000", - // "min_query_cost": "0" - // } - // } - google.protobuf.Struct debug_stats = 5; -} diff --git a/packages/firestore/src/protos/protos.json b/packages/firestore/src/protos/protos.json index 5b73c4647f8..b2c50ccaeeb 100644 --- a/packages/firestore/src/protos/protos.json +++ b/packages/firestore/src/protos/protos.json @@ -4,78 +4,16 @@ "nested": { "protobuf": { "options": { - "go_package": "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor", + "csharp_namespace": "Google.Protobuf.WellKnownTypes", + "go_package": "github.com/golang/protobuf/ptypes/wrappers", "java_package": "com.google.protobuf", - "java_outer_classname": "DescriptorProtos", - "csharp_namespace": "Google.Protobuf.Reflection", + "java_outer_classname": "WrappersProto", + "java_multiple_files": true, "objc_class_prefix": "GPB", "cc_enable_arenas": true, "optimize_for": "SPEED" }, "nested": { - "Struct": { - "fields": { - "fields": { - "keyType": "string", - "type": "Value", - "id": 1 - } - } - }, - "Value": { - "oneofs": { - "kind": { - "oneof": [ - "nullValue", - "numberValue", - "stringValue", - "boolValue", - "structValue", - "listValue" - ] - } - }, - "fields": { - "nullValue": { - "type": "NullValue", - "id": 1 - }, - "numberValue": { - "type": "double", - "id": 2 - }, - "stringValue": { - "type": "string", - "id": 3 - }, - "boolValue": { - "type": "bool", - "id": 4 - }, - "structValue": { - "type": "Struct", - "id": 5 - }, - "listValue": { - "type": "ListValue", - "id": 6 - } - } - }, - "NullValue": { - "values": { - "NULL_VALUE": 0 - } - }, - "ListValue": { - "fields": { - "values": { - "rule": "repeated", - "type": "Value", - "id": 1 - } - } - }, "Timestamp": { "fields": { "seconds": { @@ -223,10 +161,6 @@ "end": { "type": "int32", "id": 2 - }, - "options": { - "type": "ExtensionRangeOptions", - "id": 3 } } }, @@ -244,21 +178,6 @@ } } }, - "ExtensionRangeOptions": { - "fields": { - "uninterpretedOption": { - "rule": "repeated", - "type": "UninterpretedOption", - "id": 999 - } - }, - "extensions": [ - [ - 1000, - 536870911 - ] - ] - }, "FieldDescriptorProto": { "fields": { "name": { @@ -360,30 +279,6 @@ "options": { "type": "EnumOptions", "id": 3 - }, - "reservedRange": { - "rule": "repeated", - "type": "EnumReservedRange", - "id": 4 - }, - "reservedName": { - "rule": "repeated", - "type": "string", - "id": 5 - } - }, - "nested": { - "EnumReservedRange": { - "fields": { - "start": { - "type": "int32", - "id": 1 - }, - "end": { - "type": "int32", - "id": 2 - } - } } } }, @@ -440,17 +335,11 @@ }, "clientStreaming": { "type": "bool", - "id": 5, - "options": { - "default": false - } + "id": 5 }, "serverStreaming": { "type": "bool", - "id": 6, - "options": { - "default": false - } + "id": 6 } } }, @@ -466,10 +355,7 @@ }, "javaMultipleFiles": { "type": "bool", - "id": 10, - "options": { - "default": false - } + "id": 10 }, "javaGenerateEqualsAndHash": { "type": "bool", @@ -480,10 +366,7 @@ }, "javaStringCheckUtf8": { "type": "bool", - "id": 27, - "options": { - "default": false - } + "id": 27 }, "optimizeFor": { "type": "OptimizeMode", @@ -498,45 +381,23 @@ }, "ccGenericServices": { "type": "bool", - "id": 16, - "options": { - "default": false - } + "id": 16 }, "javaGenericServices": { "type": "bool", - "id": 17, - "options": { - "default": false - } + "id": 17 }, "pyGenericServices": { "type": "bool", - "id": 18, - "options": { - "default": false - } - }, - "phpGenericServices": { - "type": "bool", - "id": 42, - "options": { - "default": false - } + "id": 18 }, "deprecated": { "type": "bool", - "id": 23, - "options": { - "default": false - } + "id": 23 }, "ccEnableArenas": { "type": "bool", - "id": 31, - "options": { - "default": false - } + "id": 31 }, "objcClassPrefix": { "type": "string", @@ -546,26 +407,6 @@ "type": "string", "id": 37 }, - "swiftPrefix": { - "type": "string", - "id": 39 - }, - "phpClassPrefix": { - "type": "string", - "id": 40 - }, - "phpNamespace": { - "type": "string", - "id": 41 - }, - "phpMetadataNamespace": { - "type": "string", - "id": 44 - }, - "rubyPackage": { - "type": "string", - "id": 45 - }, "uninterpretedOption": { "rule": "repeated", "type": "UninterpretedOption", @@ -598,24 +439,15 @@ "fields": { "messageSetWireFormat": { "type": "bool", - "id": 1, - "options": { - "default": false - } + "id": 1 }, "noStandardDescriptorAccessor": { "type": "bool", - "id": 2, - "options": { - "default": false - } + "id": 2 }, "deprecated": { "type": "bool", - "id": 3, - "options": { - "default": false - } + "id": 3 }, "mapEntry": { "type": "bool", @@ -637,10 +469,6 @@ [ 8, 8 - ], - [ - 9, - 9 ] ] }, @@ -666,24 +494,15 @@ }, "lazy": { "type": "bool", - "id": 5, - "options": { - "default": false - } + "id": 5 }, "deprecated": { "type": "bool", - "id": 3, - "options": { - "default": false - } + "id": 3 }, "weak": { "type": "bool", - "id": 10, - "options": { - "default": false - } + "id": 10 }, "uninterpretedOption": { "rule": "repeated", @@ -743,10 +562,7 @@ }, "deprecated": { "type": "bool", - "id": 3, - "options": { - "default": false - } + "id": 3 }, "uninterpretedOption": { "rule": "repeated", @@ -759,22 +575,13 @@ 1000, 536870911 ] - ], - "reserved": [ - [ - 5, - 5 - ] ] }, "EnumValueOptions": { "fields": { "deprecated": { "type": "bool", - "id": 1, - "options": { - "default": false - } + "id": 1 }, "uninterpretedOption": { "rule": "repeated", @@ -793,10 +600,7 @@ "fields": { "deprecated": { "type": "bool", - "id": 33, - "options": { - "default": false - } + "id": 33 }, "uninterpretedOption": { "rule": "repeated", @@ -815,17 +619,7 @@ "fields": { "deprecated": { "type": "bool", - "id": 33, - "options": { - "default": false - } - }, - "idempotencyLevel": { - "type": "IdempotencyLevel", - "id": 34, - "options": { - "default": "IDEMPOTENCY_UNKNOWN" - } + "id": 33 }, "uninterpretedOption": { "rule": "repeated", @@ -838,16 +632,7 @@ 1000, 536870911 ] - ], - "nested": { - "IdempotencyLevel": { - "values": { - "IDEMPOTENCY_UNKNOWN": 0, - "NO_SIDE_EFFECTS": 1, - "IDEMPOTENT": 2 - } - } - } + ] }, "UninterpretedOption": { "fields": { @@ -968,6 +753,72 @@ } } }, + "Struct": { + "fields": { + "fields": { + "keyType": "string", + "type": "Value", + "id": 1 + } + } + }, + "Value": { + "oneofs": { + "kind": { + "oneof": [ + "nullValue", + "numberValue", + "stringValue", + "boolValue", + "structValue", + "listValue" + ] + } + }, + "fields": { + "nullValue": { + "type": "NullValue", + "id": 1 + }, + "numberValue": { + "type": "double", + "id": 2 + }, + "stringValue": { + "type": "string", + "id": 3 + }, + "boolValue": { + "type": "bool", + "id": 4 + }, + "structValue": { + "type": "Struct", + "id": 5 + }, + "listValue": { + "type": "ListValue", + "id": 6 + } + } + }, + "NullValue": { + "values": { + "NULL_VALUE": 0 + } + }, + "ListValue": { + "fields": { + "values": { + "rule": "repeated", + "type": "Value", + "id": 1 + } + } + }, + "Empty": { + "fields": {} + }, "DoubleValue": { "fields": { "value": { @@ -1040,12 +891,9 @@ } } }, - "Empty": { - "fields": {} - }, "Any": { "fields": { - "type_url": { + "typeUrl": { "type": "string", "id": 1 }, @@ -1054,18 +902,6 @@ "id": 2 } } - }, - "Duration": { - "fields": { - "seconds": { - "type": "int64", - "id": 1 - }, - "nanos": { - "type": "int32", - "id": 2 - } - } } } }, @@ -1074,9 +910,9 @@ "v1": { "options": { "csharp_namespace": "Google.Cloud.Firestore.V1", - "go_package": "cloud.google.com/go/firestore/apiv1/firestorepb;firestorepb", + "go_package": "google.golang.org/genproto/googleapis/firestore/v1;firestore", "java_multiple_files": true, - "java_outer_classname": "QueryProfileProto", + "java_outer_classname": "WriteProto", "java_package": "com.google.firestore.v1", "objc_class_prefix": "GCFS", "php_namespace": "Google\\Cloud\\Firestore\\V1", @@ -1092,6 +928,104 @@ } } }, + "BitSequence": { + "fields": { + "bitmap": { + "type": "bytes", + "id": 1 + }, + "padding": { + "type": "int32", + "id": 2 + } + } + }, + "BloomFilter": { + "fields": { + "bits": { + "type": "BitSequence", + "id": 1 + }, + "hashCount": { + "type": "int32", + "id": 2 + } + } + }, + "DocumentMask": { + "fields": { + "fieldPaths": { + "rule": "repeated", + "type": "string", + "id": 1 + } + } + }, + "Precondition": { + "oneofs": { + "conditionType": { + "oneof": [ + "exists", + "updateTime" + ] + } + }, + "fields": { + "exists": { + "type": "bool", + "id": 1 + }, + "updateTime": { + "type": "google.protobuf.Timestamp", + "id": 2 + } + } + }, + "TransactionOptions": { + "oneofs": { + "mode": { + "oneof": [ + "readOnly", + "readWrite" + ] + } + }, + "fields": { + "readOnly": { + "type": "ReadOnly", + "id": 2 + }, + "readWrite": { + "type": "ReadWrite", + "id": 3 + } + }, + "nested": { + "ReadWrite": { + "fields": { + "retryTransaction": { + "type": "bytes", + "id": 1 + } + } + }, + "ReadOnly": { + "oneofs": { + "consistencySelector": { + "oneof": [ + "readTime" + ] + } + }, + "fields": { + "readTime": { + "type": "google.protobuf.Timestamp", + "id": 2 + } + } + } + } + }, "Document": { "fields": { "name": { @@ -1127,10 +1061,7 @@ "referenceValue", "geoPointValue", "arrayValue", - "mapValue", - "fieldReferenceValue", - "functionValue", - "pipelineValue" + "mapValue" ] } }, @@ -1163,196 +1094,39 @@ "type": "bytes", "id": 18 }, - "referenceValue": { - "type": "string", - "id": 5 - }, - "geoPointValue": { - "type": "google.type.LatLng", - "id": 8 - }, - "arrayValue": { - "type": "ArrayValue", - "id": 9 - }, - "mapValue": { - "type": "MapValue", - "id": 6 - }, - "fieldReferenceValue": { - "type": "string", - "id": 19 - }, - "functionValue": { - "type": "Function", - "id": 20 - }, - "pipelineValue": { - "type": "Pipeline", - "id": 21 - } - } - }, - "ArrayValue": { - "fields": { - "values": { - "rule": "repeated", - "type": "Value", - "id": 1 - } - } - }, - "MapValue": { - "fields": { - "fields": { - "keyType": "string", - "type": "Value", - "id": 1 - } - } - }, - "Function": { - "fields": { - "name": { - "type": "string", - "id": 1 - }, - "args": { - "rule": "repeated", - "type": "Value", - "id": 2 - }, - "options": { - "keyType": "string", - "type": "Value", - "id": 3 - } - } - }, - "Pipeline": { - "fields": { - "stages": { - "rule": "repeated", - "type": "Stage", - "id": 1 - } - }, - "nested": { - "Stage": { - "fields": { - "name": { - "type": "string", - "id": 1 - }, - "args": { - "rule": "repeated", - "type": "Value", - "id": 2 - }, - "options": { - "keyType": "string", - "type": "Value", - "id": 3 - } - } - } - } - }, - "BitSequence": { - "fields": { - "bitmap": { - "type": "bytes", - "id": 1 - }, - "padding": { - "type": "int32", - "id": 2 - } - } - }, - "BloomFilter": { - "fields": { - "bits": { - "type": "BitSequence", - "id": 1 + "referenceValue": { + "type": "string", + "id": 5 }, - "hashCount": { - "type": "int32", - "id": 2 + "geoPointValue": { + "type": "google.type.LatLng", + "id": 8 + }, + "arrayValue": { + "type": "ArrayValue", + "id": 9 + }, + "mapValue": { + "type": "MapValue", + "id": 6 } } }, - "DocumentMask": { + "ArrayValue": { "fields": { - "fieldPaths": { + "values": { "rule": "repeated", - "type": "string", + "type": "Value", "id": 1 } } }, - "Precondition": { - "oneofs": { - "conditionType": { - "oneof": [ - "exists", - "updateTime" - ] - } - }, + "MapValue": { "fields": { - "exists": { - "type": "bool", + "fields": { + "keyType": "string", + "type": "Value", "id": 1 - }, - "updateTime": { - "type": "google.protobuf.Timestamp", - "id": 2 - } - } - }, - "TransactionOptions": { - "oneofs": { - "mode": { - "oneof": [ - "readOnly", - "readWrite" - ] - } - }, - "fields": { - "readOnly": { - "type": "ReadOnly", - "id": 2 - }, - "readWrite": { - "type": "ReadWrite", - "id": 3 - } - }, - "nested": { - "ReadWrite": { - "fields": { - "retryTransaction": { - "type": "bytes", - "id": 1 - } - } - }, - "ReadOnly": { - "oneofs": { - "consistencySelector": { - "oneof": [ - "readTime" - ] - } - }, - "fields": { - "readTime": { - "type": "google.protobuf.Timestamp", - "id": 2 - } - } } } }, @@ -1528,23 +1302,6 @@ } ] }, - "ExecutePipeline": { - "requestType": "ExecutePipelineRequest", - "responseType": "ExecutePipelineResponse", - "responseStream": true, - "options": { - "(google.api.http).post": "/v1/{database=projects/*/databases/*}/documents:executePipeline", - "(google.api.http).body": "*" - }, - "parsedOptions": [ - { - "(google.api.http)": { - "post": "/v1/{database=projects/*/databases/*}/documents:executePipeline", - "body": "*" - } - } - ] - }, "RunAggregationQuery": { "requestType": "RunAggregationQueryRequest", "responseType": "RunAggregationQueryResponse", @@ -2059,64 +1816,6 @@ } } }, - "ExecutePipelineRequest": { - "oneofs": { - "pipelineType": { - "oneof": [ - "structuredPipeline" - ] - }, - "consistencySelector": { - "oneof": [ - "transaction", - "newTransaction", - "readTime" - ] - } - }, - "fields": { - "database": { - "type": "string", - "id": 1, - "options": { - "(google.api.field_behavior)": "REQUIRED" - } - }, - "structuredPipeline": { - "type": "StructuredPipeline", - "id": 2 - }, - "transaction": { - "type": "bytes", - "id": 5 - }, - "newTransaction": { - "type": "TransactionOptions", - "id": 6 - }, - "readTime": { - "type": "google.protobuf.Timestamp", - "id": 7 - } - } - }, - "ExecutePipelineResponse": { - "fields": { - "transaction": { - "type": "bytes", - "id": 1 - }, - "results": { - "rule": "repeated", - "type": "Document", - "id": 2 - }, - "executionTime": { - "type": "google.protobuf.Timestamp", - "id": 3 - } - } - }, "RunAggregationQueryRequest": { "oneofs": { "queryType": { @@ -2517,19 +2216,6 @@ } } }, - "StructuredPipeline": { - "fields": { - "pipeline": { - "type": "Pipeline", - "id": 1 - }, - "options": { - "keyType": "string", - "type": "Value", - "id": 2 - } - } - }, "StructuredQuery": { "fields": { "select": { @@ -2788,7 +2474,7 @@ "Sum": { "fields": { "field": { - "type": "StructuredQuery.FieldReference", + "type": "FieldReference", "id": 1 } } @@ -2796,7 +2482,7 @@ "Avg": { "fields": { "field": { - "type": "StructuredQuery.FieldReference", + "type": "FieldReference", "id": 1 } } @@ -3008,82 +2694,6 @@ "id": 3 } } - }, - "ExplainOptions": { - "fields": { - "analyze": { - "type": "bool", - "id": 1, - "options": { - "(google.api.field_behavior)": "OPTIONAL" - } - } - } - }, - "ExplainMetrics": { - "fields": { - "planSummary": { - "type": "PlanSummary", - "id": 1 - }, - "executionStats": { - "type": "ExecutionStats", - "id": 2 - } - } - }, - "PlanSummary": { - "fields": { - "indexesUsed": { - "rule": "repeated", - "type": "google.protobuf.Struct", - "id": 1 - } - } - }, - "ExecutionStats": { - "fields": { - "resultsReturned": { - "type": "int64", - "id": 1 - }, - "executionDuration": { - "type": "google.protobuf.Duration", - "id": 3 - }, - "readOperations": { - "type": "int64", - "id": 4 - }, - "debugStats": { - "type": "google.protobuf.Struct", - "id": 5 - } - } - } - } - } - } - }, - "type": { - "options": { - "cc_enable_arenas": true, - "go_package": "google.golang.org/genproto/googleapis/type/latlng;latlng", - "java_multiple_files": true, - "java_outer_classname": "LatLngProto", - "java_package": "com.google.type", - "objc_class_prefix": "GTP" - }, - "nested": { - "LatLng": { - "fields": { - "latitude": { - "type": "double", - "id": 1 - }, - "longitude": { - "type": "double", - "id": 2 } } } @@ -3091,9 +2701,9 @@ }, "api": { "options": { - "go_package": "google.golang.org/genproto/googleapis/api;api", + "go_package": "google.golang.org/genproto/googleapis/api/annotations;annotations", "java_multiple_files": true, - "java_outer_classname": "LaunchStageProto", + "java_outer_classname": "HttpProto", "java_package": "com.google.api", "objc_class_prefix": "GAPI", "cc_enable_arenas": true @@ -3110,10 +2720,6 @@ "rule": "repeated", "type": "HttpRule", "id": 1 - }, - "fullyDecodeReservedExpansion": { - "type": "bool", - "id": 2 } } }, @@ -3131,10 +2737,6 @@ } }, "fields": { - "selector": { - "type": "string", - "id": 1 - }, "get": { "type": "string", "id": 2 @@ -3159,13 +2761,13 @@ "type": "CustomHttpPattern", "id": 8 }, - "body": { + "selector": { "type": "string", - "id": 7 + "id": 1 }, - "responseBody": { + "body": { "type": "string", - "id": 12 + "id": 7 }, "additionalBindings": { "rule": "repeated", @@ -3219,17 +2821,29 @@ "UNORDERED_LIST": 6, "NON_EMPTY_DEFAULT": 7 } - }, - "LaunchStage": { - "values": { - "LAUNCH_STAGE_UNSPECIFIED": 0, - "UNIMPLEMENTED": 6, - "PRELAUNCH": 7, - "EARLY_ACCESS": 1, - "ALPHA": 2, - "BETA": 3, - "GA": 4, - "DEPRECATED": 5 + } + } + }, + "type": { + "options": { + "cc_enable_arenas": true, + "go_package": "google.golang.org/genproto/googleapis/type/latlng;latlng", + "java_multiple_files": true, + "java_outer_classname": "LatLngProto", + "java_package": "com.google.type", + "objc_class_prefix": "GTP" + }, + "nested": { + "LatLng": { + "fields": { + "latitude": { + "type": "double", + "id": 1 + }, + "longitude": { + "type": "double", + "id": 2 + } } } } @@ -3266,4 +2880,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/firestore/src/register.ts b/packages/firestore/src/register.ts index 82b450b3834..7ec6aae5c30 100644 --- a/packages/firestore/src/register.ts +++ b/packages/firestore/src/register.ts @@ -61,6 +61,6 @@ export function registerFirestore( ).setMultipleInstances(true) ); registerVersion(name, version, variant); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index ee27786ac9f..f790ede0d5c 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -20,12 +20,10 @@ import { User } from '../auth/user'; import { Aggregate } from '../core/aggregate'; import { DatabaseId } from '../core/database_info'; import { queryToAggregateTarget, Query, queryToTarget } from '../core/query'; -import { Pipeline } from '../lite-api/pipeline'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; import { ResourcePath } from '../model/path'; -import { PipelineStreamElement } from '../model/pipeline_stream_element'; import { ApiClientObjectMap, BatchGetDocumentsRequest as ProtoBatchGetDocumentsRequest, @@ -34,8 +32,6 @@ import { RunAggregationQueryResponse as ProtoRunAggregationQueryResponse, RunQueryRequest as ProtoRunQueryRequest, RunQueryResponse as ProtoRunQueryResponse, - ExecutePipelineRequest as ProtoExecutePipelineRequest, - ExecutePipelineResponse as ProtoExecutePipelineResponse, Value } from '../protos/firestore_proto_api'; import { debugAssert, debugCast, hardAssert } from '../util/assert'; @@ -58,8 +54,7 @@ import { toName, toQueryTarget, toResourcePath, - toRunAggregationQueryRequest, - fromPipelineResponse + toRunAggregationQueryRequest } from './serializer'; /** @@ -241,36 +236,6 @@ export async function invokeBatchGetDocumentsRpc( return result; } -export async function invokeExecutePipeline( - datastore: Datastore, - pipeline: Pipeline -): Promise { - const datastoreImpl = debugCast(datastore, DatastoreImpl); - const executePipelineRequest = pipeline._toProto(datastoreImpl.serializer); - - const response = await datastoreImpl.invokeStreamingRPC< - ProtoExecutePipelineRequest, - ProtoExecutePipelineResponse - >( - 'ExecutePipeline', - datastoreImpl.serializer.databaseId, - ResourcePath.emptyPath(), - executePipelineRequest - ); - - return response - .filter(proto => !!proto.results) - .flatMap(proto => { - if (proto.results!.length === 0) { - return fromPipelineResponse(datastoreImpl.serializer, proto); - } else { - return proto.results!.map(result => - fromPipelineResponse(datastoreImpl.serializer, proto, result) - ); - } - }); -} - export async function invokeRunQueryRpc( datastore: Datastore, query: Query diff --git a/packages/firestore/src/remote/internal_serializer.ts b/packages/firestore/src/remote/internal_serializer.ts index 29a68620efc..8f278247581 100644 --- a/packages/firestore/src/remote/internal_serializer.ts +++ b/packages/firestore/src/remote/internal_serializer.ts @@ -19,8 +19,6 @@ import { ensureFirestoreConfigured, Firestore } from '../api/database'; import { AggregateImpl } from '../core/aggregate'; import { queryToAggregateTarget, queryToTarget } from '../core/query'; import { AggregateSpec } from '../lite-api/aggregate_types'; -import { getDatastore } from '../lite-api/components'; -import { Pipeline } from '../lite-api/pipeline'; import { Query } from '../lite-api/reference'; import { cast } from '../util/input_validation'; import { mapToArray } from '../util/obj'; @@ -89,28 +87,3 @@ export function _internalAggregationQueryToProtoRunAggregationQueryRequest< /* skipAliasing= */ true ).request; } - -/** - * @internal - * @private - * - * This function is for internal use only. - * - * Returns the `ExecutePipelineRequest` representation of the given query. - * Returns `null` if the Firestore client associated with the given query has - * not been initialized or has been terminated. - * - * @param pipeline - The Pipeline to convert to proto representation. - */ -export function _internalPipelineToExecutePipelineRequestProto( - pipeline: Pipeline - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): any { - const firestore = cast(pipeline._db, Firestore); - const datastore = getDatastore(firestore); - const serializer = datastore.serializer; - if (serializer === undefined) { - return null; - } - return pipeline._toProto(serializer); -} diff --git a/packages/firestore/src/remote/rest_connection.ts b/packages/firestore/src/remote/rest_connection.ts index 7469d8f45ff..2d6889dac3b 100644 --- a/packages/firestore/src/remote/rest_connection.ts +++ b/packages/firestore/src/remote/rest_connection.ts @@ -46,7 +46,6 @@ RPC_NAME_URL_MAPPING['BatchGetDocuments'] = 'batchGet'; RPC_NAME_URL_MAPPING['Commit'] = 'commit'; RPC_NAME_URL_MAPPING['RunQuery'] = 'runQuery'; RPC_NAME_URL_MAPPING['RunAggregationQuery'] = 'runAggregationQuery'; -RPC_NAME_URL_MAPPING['ExecutePipeline'] = 'executePipeline'; const RPC_URL_VERSION = 'v1'; diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 9a683ffe47c..830875f5e1b 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -37,10 +37,7 @@ import { import { SnapshotVersion } from '../core/snapshot_version'; import { targetIsDocumentTarget, Target } from '../core/target'; import { TargetId } from '../core/types'; -import { Bytes } from '../lite-api/bytes'; -import { GeoPoint } from '../lite-api/geo_point'; import { Timestamp } from '../lite-api/timestamp'; -import { UserDataReader } from '../lite-api/user_data_reader'; import { TargetData, TargetPurpose } from '../local/target_data'; import { MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -58,7 +55,6 @@ import { import { normalizeTimestamp } from '../model/normalize'; import { ObjectValue } from '../model/object_value'; import { FieldPath, ResourcePath } from '../model/path'; -import { PipelineStreamElement } from '../model/pipeline_stream_element'; import { ArrayRemoveTransformOperation, ArrayUnionTransformOperation, @@ -91,10 +87,7 @@ import { TargetChangeTargetChangeType as ProtoTargetChangeTargetChangeType, Timestamp as ProtoTimestamp, Write as ProtoWrite, - WriteResult as ProtoWriteResult, - Value as ProtoValue, - MapValue as ProtoMapValue, - ExecutePipelineResponse as ProtoExecutePipelineResponse + WriteResult as ProtoWriteResult } from '../protos/firestore_proto_api'; import { debugAssert, fail, hardAssert } from '../util/assert'; import { ByteString } from '../util/byte_string'; @@ -180,7 +173,7 @@ function fromRpcStatus(status: ProtoStatus): FirestoreError { * our generated proto interfaces say Int32Value must be. But GRPC actually * expects a { value: } struct. */ -export function toInt32Proto( +function toInt32Proto( serializer: JsonProtoSerializer, val: number | null ): number | { value: number } | null { @@ -438,37 +431,6 @@ export function toDocument( }; } -export function fromPipelineResponse( - serializer: JsonProtoSerializer, - proto: ProtoExecutePipelineResponse, - document?: ProtoDocument -): PipelineStreamElement { - const output: PipelineStreamElement = {}; - if (proto.transaction?.length) { - output.transaction = proto.transaction; - } - const executionTime = proto.executionTime - ? fromVersion(proto.executionTime) - : undefined; - output.executionTime = executionTime; - - if (!!document) { - output.key = document.name - ? fromName(serializer, document.name) - : undefined; - - output.fields = new ObjectValue({ mapValue: { fields: document.fields } }); - - output.createTime = document.createTime - ? fromVersion(document.createTime!) - : undefined; - output.updateTime = document.updateTime - ? fromVersion(document.updateTime!) - : undefined; - } - return output; -} - export function fromDocument( serializer: JsonProtoSerializer, document: ProtoDocument, @@ -1452,82 +1414,3 @@ export function isValidResourceName(path: ResourcePath): boolean { path.get(2) === 'databases' ); } - -export interface ProtoSerializable { - _toProto(serializer: JsonProtoSerializer): ProtoType; -} - -export interface UserData { - _readUserData(dataReader: UserDataReader): void; -} - -export function toMapValue( - serializer: JsonProtoSerializer, - input: Map> -): ProtoValue { - const map: ProtoMapValue = { fields: {} }; - input.forEach((exp: ProtoSerializable, key: string) => { - if (typeof key !== 'string') { - throw new Error(`Cannot encode map with non-string key: ${key}`); - } - - map.fields![key] = exp._toProto(serializer)!; - }); - return { - mapValue: map - }; -} - -export function toNullValue(value: null): ProtoValue { - return { nullValue: 'NULL_VALUE' }; -} - -export function toBooleanValue(value: boolean): ProtoValue { - return { booleanValue: value }; -} - -export function toStringValue(value: string): ProtoValue { - return { stringValue: value }; -} - -export function dateToTimestampValue( - serializer: JsonProtoSerializer, - value: Date -): ProtoValue { - const timestamp = Timestamp.fromDate(value); - return { - timestampValue: toTimestamp(serializer, timestamp) - }; -} - -export function timestampToTimestampValue( - serializer: JsonProtoSerializer, - value: Timestamp -): ProtoValue { - // Firestore backend truncates precision down to microseconds. To ensure - // offline mode works the same in regards to truncation, perform the - // truncation immediately without waiting for the backend to do that. - const timestamp = new Timestamp( - value.seconds, - Math.floor(value.nanoseconds / 1000) * 1000 - ); - return { - timestampValue: toTimestamp(serializer, timestamp) - }; -} - -export function toGeoPointValue(value: GeoPoint): ProtoValue { - return { - geoPointValue: { - latitude: value.latitude, - longitude: value.longitude - } - }; -} - -export function toBytesValue( - serializer: JsonProtoSerializer, - value: Bytes -): ProtoValue { - return { bytesValue: toBytes(serializer, value._byteString) }; -} diff --git a/packages/firestore/src/util/misc.ts b/packages/firestore/src/util/misc.ts index 79fbc27e3f7..f2fa04d1b43 100644 --- a/packages/firestore/src/util/misc.ts +++ b/packages/firestore/src/util/misc.ts @@ -16,7 +16,6 @@ */ import { randomBytes } from '../platform/random_bytes'; -import { newTextEncoder } from '../platform/text_serializer'; import { debugAssert } from './assert'; @@ -77,63 +76,66 @@ export interface Equatable { /** Compare strings in UTF-8 encoded byte order */ export function compareUtf8Strings(left: string, right: string): number { - let i = 0; - while (i < left.length && i < right.length) { - const leftCodePoint = left.codePointAt(i)!; - const rightCodePoint = right.codePointAt(i)!; - - if (leftCodePoint !== rightCodePoint) { - if (leftCodePoint < 128 && rightCodePoint < 128) { - // ASCII comparison - return primitiveComparator(leftCodePoint, rightCodePoint); - } else { - // Lazy instantiate TextEncoder - const encoder = newTextEncoder(); - - // UTF-8 encode the character at index i for byte comparison. - const leftBytes = encoder.encode(getUtf8SafeSubstring(left, i)); - const rightBytes = encoder.encode(getUtf8SafeSubstring(right, i)); - - const comp = compareByteArrays(leftBytes, rightBytes); - if (comp !== 0) { - return comp; - } else { - // EXTREMELY RARE CASE: Code points differ, but their UTF-8 byte - // representations are identical. This can happen with malformed input - // (invalid surrogate pairs). The backend also actively prevents invalid - // surrogates as INVALID_ARGUMENT errors, so we almost never receive - // invalid strings from backend. - // Fallback to code point comparison for graceful handling. - return primitiveComparator(leftCodePoint, rightCodePoint); - } - } + // Find the first differing character (a.k.a. "UTF-16 code unit") in the two strings and, + // if found, use that character to determine the relative ordering of the two strings as a + // whole. Comparing UTF-16 strings in UTF-8 byte order can be done simply and efficiently by + // comparing the UTF-16 code units (chars). This serendipitously works because of the way UTF-8 + // and UTF-16 happen to represent Unicode code points. + // + // After finding the first pair of differing characters, there are two cases: + // + // Case 1: Both characters are non-surrogates (code points less than or equal to 0xFFFF) or + // both are surrogates from a surrogate pair (that collectively represent code points greater + // than 0xFFFF). In this case their numeric order as UTF-16 code units is the same as the + // lexicographical order of their corresponding UTF-8 byte sequences. A direct comparison is + // sufficient. + // + // Case 2: One character is a surrogate and the other is not. In this case the surrogate- + // containing string is always ordered after the non-surrogate. This is because surrogates are + // used to represent code points greater than 0xFFFF which have 4-byte UTF-8 representations + // and are lexicographically greater than the 1, 2, or 3-byte representations of code points + // less than or equal to 0xFFFF. + // + // An example of why Case 2 is required is comparing the following two Unicode code points: + // + // |-----------------------|------------|---------------------|-----------------| + // | Name | Code Point | UTF-8 Encoding | UTF-16 Encoding | + // |-----------------------|------------|---------------------|-----------------| + // | Replacement Character | U+FFFD | 0xEF 0xBF 0xBD | 0xFFFD | + // | Grinning Face | U+1F600 | 0xF0 0x9F 0x98 0x80 | 0xD83D 0xDE00 | + // |-----------------------|------------|---------------------|-----------------| + // + // A lexicographical comparison of the UTF-8 encodings of these code points would order + // "Replacement Character" _before_ "Grinning Face" because 0xEF is less than 0xF0. However, a + // direct comparison of the UTF-16 code units, as would be done in case 1, would erroneously + // produce the _opposite_ ordering, because 0xFFFD is _greater than_ 0xD83D. As it turns out, + // this relative ordering holds for all comparisons of UTF-16 code points requiring a surrogate + // pair with those that do not. + const length = Math.min(left.length, right.length); + for (let i = 0; i < length; i++) { + const leftChar = left.charAt(i); + const rightChar = right.charAt(i); + if (leftChar !== rightChar) { + return isSurrogate(leftChar) === isSurrogate(rightChar) + ? primitiveComparator(leftChar, rightChar) + : isSurrogate(leftChar) + ? 1 + : -1; } - // Increment by 2 for surrogate pairs, 1 otherwise - i += leftCodePoint > 0xffff ? 2 : 1; } - // Compare lengths if all characters are equal + // Use the lengths of the strings to determine the overall comparison result since either the + // strings were equal or one is a prefix of the other. return primitiveComparator(left.length, right.length); } -function getUtf8SafeSubstring(str: string, index: number): string { - const firstCodePoint = str.codePointAt(index)!; - if (firstCodePoint > 0xffff) { - // It's a surrogate pair, return the whole pair - return str.substring(index, index + 2); - } else { - // It's a single code point, return it - return str.substring(index, index + 1); - } -} +const MIN_SURROGATE = 0xd800; +const MAX_SURROGATE = 0xdfff; -function compareByteArrays(left: Uint8Array, right: Uint8Array): number { - for (let i = 0; i < left.length && i < right.length; ++i) { - if (left[i] !== right[i]) { - return primitiveComparator(left[i], right[i]); - } - } - return primitiveComparator(left.length, right.length); +export function isSurrogate(s: string): boolean { + debugAssert(s.length === 1, `s.length == ${s.length}, but expected 1`); + const c = s.charCodeAt(0); + return c >= MIN_SURROGATE && c <= MAX_SURROGATE; } export interface Iterable { @@ -151,26 +153,6 @@ export function arrayEquals( } return left.every((value, index) => comparator(value, right[index])); } - -/** - * Verifies equality for an optional value. - */ -export function isOptionalEqual( - left: T | undefined, - right: T | undefined, - equalityTest: (left: T, right: T) => boolean -): boolean { - if (left === undefined && right === undefined) { - return true; - } - - if (left === undefined || right === undefined) { - return false; - } - - return equalityTest(left, right); -} - /** * Returns the immediate lexicographically-following string. This is useful to * construct an inclusive range for indexeddb iterators. diff --git a/packages/firestore/src/util/obj.ts b/packages/firestore/src/util/obj.ts index 2b61da9447f..c40bc86bc5c 100644 --- a/packages/firestore/src/util/obj.ts +++ b/packages/firestore/src/util/obj.ts @@ -32,7 +32,7 @@ export function objectSize(obj: object): number { } export function forEach( - obj: Record | undefined, + obj: Dict | undefined, fn: (key: string, val: V) => void ): void { for (const key in obj) { diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts deleted file mode 100644 index a954d9b53e1..00000000000 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ /dev/null @@ -1,892 +0,0 @@ -/** - * @license - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; - -import { addEqualityMatcher } from '../../util/equality_matcher'; -import { Deferred } from '../../util/promise'; -import { - pipeline, - execute, - _internalPipelineToExecutePipelineRequestProto, - add, - andFunction, - arrayContains, - arrayContainsAny, - avgFunction, - CollectionReference, - Constant, - cosineDistance, - countAll, - doc, - DocumentData, - dotProduct, - endsWith, - eq, - euclideanDistance, - Field, - Firestore, - gt, - like, - lt, - lte, - mapGet, - neq, - not, - orFunction, - PipelineResult, - regexContains, - regexMatch, - setDoc, - startsWith, - subtract, - useFirestorePipelines -} from '../util/firebase_export'; -import { apiDescribe, withTestCollection } from '../util/helpers'; - -use(chaiAsPromised); -useFirestorePipelines(); - -apiDescribe.skip('Pipelines', persistence => { - addEqualityMatcher(); - let firestore: Firestore; - let randomCol: CollectionReference; - - // async function addDocs( - // ...docs: DocumentData[] - // ): Promise { - // let id = 0; // Guarantees consistent ordering for the first documents - // const refs: DocumentReference[] = []; - // for (const data of docs) { - // const ref = doc(randomCol, 'doc' + id++); - // await setDoc(ref, data); - // refs.push(ref); - // } - // return refs; - // } - - async function testCollectionWithDocs(docs: { - [id: string]: DocumentData; - }): Promise> { - for (const id in docs) { - if (docs.hasOwnProperty(id)) { - const ref = doc(randomCol, id); - await setDoc(ref, docs[id]); - } - } - return randomCol; - } - - function expectResults( - result: Array>, - ...docs: string[] - ): void; - function expectResults( - result: Array>, - ...data: DocumentData[] - ): void; - - function expectResults( - result: Array>, - ...data: DocumentData[] | string[] - ): void { - expect(result.length).to.equal(data.length); - - if (data.length > 0) { - if (typeof data[0] === 'string') { - const actualIds = result.map(result => result.ref?.id); - expect(actualIds).to.deep.equal(data); - } else { - result.forEach(r => { - expect(r.data()).to.deep.equal(data.shift()); - }); - } - } - } - - // async function compareQueryAndPipeline(query: Query): Promise { - // const queryResults = await getDocs(query); - // const pipeline = query.pipeline(); - // const pipelineResults = await pipeline.execute(); - // - // expect(queryResults.docs.map(s => s._fieldsProto)).to.deep.equal( - // pipelineResults.map(r => r._fieldsProto) - // ); - // return queryResults; - // } - - async function setupBookDocs(): Promise> { - const bookDocs: { [id: string]: DocumentData } = { - book1: { - title: "The Hitchhiker's Guide to the Galaxy", - author: 'Douglas Adams', - genre: 'Science Fiction', - published: 1979, - rating: 4.2, - tags: ['comedy', 'space', 'adventure'], - awards: { - hugo: true, - nebula: false, - others: { unknown: { year: 1980 } } - }, - nestedField: { 'level.1': { 'level.2': true } } - }, - book2: { - title: 'Pride and Prejudice', - author: 'Jane Austen', - genre: 'Romance', - published: 1813, - rating: 4.5, - tags: ['classic', 'social commentary', 'love'], - awards: { none: true } - }, - book3: { - title: 'One Hundred Years of Solitude', - author: 'Gabriel García Márquez', - genre: 'Magical Realism', - published: 1967, - rating: 4.3, - tags: ['family', 'history', 'fantasy'], - awards: { nobel: true, nebula: false } - }, - book4: { - title: 'The Lord of the Rings', - author: 'J.R.R. Tolkien', - genre: 'Fantasy', - published: 1954, - rating: 4.7, - tags: ['adventure', 'magic', 'epic'], - awards: { hugo: false, nebula: false } - }, - book5: { - title: "The Handmaid's Tale", - author: 'Margaret Atwood', - genre: 'Dystopian', - published: 1985, - rating: 4.1, - tags: ['feminism', 'totalitarianism', 'resistance'], - awards: { 'arthur c. clarke': true, 'booker prize': false } - }, - book6: { - title: 'Crime and Punishment', - author: 'Fyodor Dostoevsky', - genre: 'Psychological Thriller', - published: 1866, - rating: 4.3, - tags: ['philosophy', 'crime', 'redemption'], - awards: { none: true } - }, - book7: { - title: 'To Kill a Mockingbird', - author: 'Harper Lee', - genre: 'Southern Gothic', - published: 1960, - rating: 4.2, - tags: ['racism', 'injustice', 'coming-of-age'], - awards: { pulitzer: true } - }, - book8: { - title: '1984', - author: 'George Orwell', - genre: 'Dystopian', - published: 1949, - rating: 4.2, - tags: ['surveillance', 'totalitarianism', 'propaganda'], - awards: { prometheus: true } - }, - book9: { - title: 'The Great Gatsby', - author: 'F. Scott Fitzgerald', - genre: 'Modernist', - published: 1925, - rating: 4.0, - tags: ['wealth', 'american dream', 'love'], - awards: { none: true } - }, - book10: { - title: 'Dune', - author: 'Frank Herbert', - genre: 'Science Fiction', - published: 1965, - rating: 4.6, - tags: ['politics', 'desert', 'ecology'], - awards: { hugo: true, nebula: true } - } - }; - return testCollectionWithDocs(bookDocs); - } - - let testDeferred: Deferred | undefined; - let withTestCollectionPromise: Promise | undefined; - - beforeEach(async () => { - const setupDeferred = new Deferred(); - testDeferred = new Deferred(); - withTestCollectionPromise = withTestCollection( - persistence, - {}, - async (collectionRef, firestoreInstance) => { - randomCol = collectionRef; - firestore = firestoreInstance; - await setupBookDocs(); - setupDeferred.resolve(); - - return testDeferred?.promise; - } - ); - - await setupDeferred.promise; - }); - - afterEach(async () => { - testDeferred?.resolve(); - await withTestCollectionPromise; - }); - - // setLogLevel('debug') - - it('empty results as expected', async () => { - const result = await firestore - .pipeline() - .collection(randomCol.path) - .limit(0) - .execute(); - expect(result.length).to.equal(0); - }); - - it('full results as expected', async () => { - const result = await firestore - .pipeline() - .collection(randomCol.path) - .execute(); - expect(result.length).to.equal(10); - }); - - it('returns aggregate results as expected', async () => { - let result = await firestore - .pipeline() - .collection(randomCol.path) - .aggregate(countAll().as('count')) - .execute(); - expectResults(result, { count: 10 }); - - result = await randomCol - .pipeline() - .where(eq('genre', 'Science Fiction')) - .aggregate( - countAll().as('count'), - avgFunction('rating').as('avgRating'), - Field.of('rating').max().as('maxRating') - ) - .execute(); - expectResults(result, { count: 2, avgRating: 4.4, maxRating: 4.6 }); - }); - - it('rejects groups without accumulators', async () => { - await expect( - randomCol - .pipeline() - .where(lt('published', 1900)) - .aggregate({ - accumulators: [], - groups: ['genre'] - }) - .execute() - ).to.be.rejected; - }); - - // skip: toLower not supported - // it.skip('returns distinct values as expected', async () => { - // const results = await randomCol - // .pipeline() - // .where(lt('published', 1900)) - // .distinct(Field.of('genre').toLower().as('lowerGenre')) - // .execute(); - // expectResults( - // results, - // { lowerGenre: 'romance' }, - // { lowerGenre: 'psychological thriller' } - // ); - // }); - - it('returns group and accumulate results', async () => { - const results = await randomCol - .pipeline() - .where(lt(Field.of('published'), 1984)) - .aggregate({ - accumulators: [avgFunction('rating').as('avgRating')], - groups: ['genre'] - }) - .where(gt('avgRating', 4.3)) - .sort(Field.of('avgRating').descending()) - .execute(); - expectResults( - results, - { avgRating: 4.7, genre: 'Fantasy' }, - { avgRating: 4.5, genre: 'Romance' }, - { avgRating: 4.4, genre: 'Science Fiction' } - ); - }); - - it('returns min and max accumulations', async () => { - const results = await randomCol - .pipeline() - .aggregate( - countAll().as('count'), - Field.of('rating').max().as('maxRating'), - Field.of('published').min().as('minPublished') - ) - .execute(); - expectResults(results, { - count: 10, - maxRating: 4.7, - minPublished: 1813 - }); - }); - - it('can select fields', async () => { - const results = await firestore - .pipeline() - .collection(randomCol.path) - .select('title', 'author') - .sort(Field.of('author').ascending()) - .execute(); - expectResults( - results, - { - title: "The Hitchhiker's Guide to the Galaxy", - author: 'Douglas Adams' - }, - { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }, - { title: 'Dune', author: 'Frank Herbert' }, - { title: 'Crime and Punishment', author: 'Fyodor Dostoevsky' }, - { - title: 'One Hundred Years of Solitude', - author: 'Gabriel García Márquez' - }, - { title: '1984', author: 'George Orwell' }, - { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, - { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' }, - { title: 'Pride and Prejudice', author: 'Jane Austen' }, - { title: "The Handmaid's Tale", author: 'Margaret Atwood' } - ); - }); - - it('where with and', async () => { - const results = await randomCol - .pipeline() - .where(andFunction(gt('rating', 4.5), eq('genre', 'Science Fiction'))) - .execute(); - expectResults(results, 'book10'); - }); - - it('where with or', async () => { - const results = await randomCol - .pipeline() - .where(orFunction(eq('genre', 'Romance'), eq('genre', 'Dystopian'))) - .select('title') - .execute(); - expectResults( - results, - { title: 'Pride and Prejudice' }, - { title: "The Handmaid's Tale" }, - { title: '1984' } - ); - }); - - it('offset and limits', async () => { - const results = await firestore - .pipeline() - .collection(randomCol.path) - .sort(Field.of('author').ascending()) - .offset(5) - .limit(3) - .select('title', 'author') - .execute(); - expectResults( - results, - { title: '1984', author: 'George Orwell' }, - { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, - { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' } - ); - }); - - it('arrayContains works', async () => { - const results = await randomCol - .pipeline() - .where(arrayContains('tags', 'comedy')) - .select('title') - .execute(); - expectResults(results, { title: "The Hitchhiker's Guide to the Galaxy" }); - }); - - it('arrayContainsAny works', async () => { - const results = await randomCol - .pipeline() - .where(arrayContainsAny('tags', ['comedy', 'classic'])) - .select('title') - .execute(); - expectResults( - results, - { title: "The Hitchhiker's Guide to the Galaxy" }, - { title: 'Pride and Prejudice' } - ); - }); - - it('arrayContainsAll works', async () => { - const results = await randomCol - .pipeline() - .where(Field.of('tags').arrayContainsAll('adventure', 'magic')) - .select('title') - .execute(); - expectResults(results, { title: 'The Lord of the Rings' }); - }); - - it('arrayLength works', async () => { - const results = await randomCol - .pipeline() - .select(Field.of('tags').arrayLength().as('tagsCount')) - .where(eq('tagsCount', 3)) - .execute(); - expect(results.length).to.equal(10); - }); - - // skip: arrayConcat not supported - // it.skip('arrayConcat works', async () => { - // const results = await randomCol - // .pipeline() - // .select( - // Field.of('tags').arrayConcat(['newTag1', 'newTag2']).as('modifiedTags') - // ) - // .limit(1) - // .execute(); - // expectResults(results, { - // modifiedTags: ['comedy', 'space', 'adventure', 'newTag1', 'newTag2'] - // }); - // }); - - it('testStrConcat', async () => { - const results = await randomCol - .pipeline() - .select( - Field.of('author').strConcat(' - ', Field.of('title')).as('bookInfo') - ) - .limit(1) - .execute(); - expectResults(results, { - bookInfo: "Douglas Adams - The Hitchhiker's Guide to the Galaxy" - }); - }); - - it('testStartsWith', async () => { - const results = await randomCol - .pipeline() - .where(startsWith('title', 'The')) - .select('title') - .sort(Field.of('title').ascending()) - .execute(); - expectResults( - results, - { title: 'The Great Gatsby' }, - { title: "The Handmaid's Tale" }, - { title: "The Hitchhiker's Guide to the Galaxy" }, - { title: 'The Lord of the Rings' } - ); - }); - - it('testEndsWith', async () => { - const results = await randomCol - .pipeline() - .where(endsWith('title', 'y')) - .select('title') - .sort(Field.of('title').descending()) - .execute(); - expectResults( - results, - { title: "The Hitchhiker's Guide to the Galaxy" }, - { title: 'The Great Gatsby' } - ); - }); - - it('testLength', async () => { - const results = await randomCol - .pipeline() - .select( - Field.of('title').charLength().as('titleLength'), - Field.of('title') - ) - .where(gt('titleLength', 20)) - .sort(Field.of('title').ascending()) - .execute(); - - expectResults( - results, - - { - titleLength: 29, - title: 'One Hundred Years of Solitude' - }, - { - titleLength: 36, - title: "The Hitchhiker's Guide to the Galaxy" - }, - { - titleLength: 21, - title: 'The Lord of the Rings' - }, - { - titleLength: 21, - title: 'To Kill a Mockingbird' - } - ); - }); - - // skip: toLower not supported - // it.skip('testToLowercase', async () => { - // const results = await randomCol - // .pipeline() - // .select(Field.of('title').toLower().as('lowercaseTitle')) - // .limit(1) - // .execute(); - // expectResults(results, { - // lowercaseTitle: "the hitchhiker's guide to the galaxy" - // }); - // }); - - // skip: toUpper not supported - // it.skip('testToUppercase', async () => { - // const results = await randomCol - // .pipeline() - // .select(Field.of('author').toUpper().as('uppercaseAuthor')) - // .limit(1) - // .execute(); - // expectResults(results, { uppercaseAuthor: 'DOUGLAS ADAMS' }); - // }); - - // skip: trim not supported - // it.skip('testTrim', async () => { - // const results = await randomCol - // .pipeline() - // .addFields(strConcat(' ', Field.of('title'), ' ').as('spacedTitle')) - // .select( - // Field.of('spacedTitle').trim().as('trimmedTitle'), - // Field.of('spacedTitle') - // ) - // .limit(1) - // .execute(); - // expectResults(results, { - // spacedTitle: " The Hitchhiker's Guide to the Galaxy ", - // trimmedTitle: "The Hitchhiker's Guide to the Galaxy" - // }); - // }); - - it('testLike', async () => { - const results = await randomCol - .pipeline() - .where(like('title', '%Guide%')) - .select('title') - .execute(); - expectResults(results, { title: "The Hitchhiker's Guide to the Galaxy" }); - }); - - it('testRegexContains', async () => { - const results = await randomCol - .pipeline() - .where(regexContains('title', '(?i)(the|of)')) - .execute(); - expect(results.length).to.equal(5); - }); - - it('testRegexMatches', async () => { - const results = await randomCol - .pipeline() - .where(regexMatch('title', '.*(?i)(the|of).*')) - .execute(); - expect(results.length).to.equal(5); - }); - - it('testArithmeticOperations', async () => { - const results = await randomCol - .pipeline() - .select( - add(Field.of('rating'), 1).as('ratingPlusOne'), - subtract(Field.of('published'), 1900).as('yearsSince1900'), - Field.of('rating').multiply(10).as('ratingTimesTen'), - Field.of('rating').divide(2).as('ratingDividedByTwo') - ) - .limit(1) - .execute(); - expectResults(results, { - ratingPlusOne: 5.2, - yearsSince1900: 79, - ratingTimesTen: 42, - ratingDividedByTwo: 2.1 - }); - }); - - it('testComparisonOperators', async () => { - const results = await randomCol - .pipeline() - .where( - andFunction( - gt('rating', 4.2), - lte(Field.of('rating'), 4.5), - neq('genre', 'Science Fiction') - ) - ) - .select('rating', 'title') - .sort(Field.of('title').ascending()) - .execute(); - expectResults( - results, - { rating: 4.3, title: 'Crime and Punishment' }, - { - rating: 4.3, - title: 'One Hundred Years of Solitude' - }, - { rating: 4.5, title: 'Pride and Prejudice' } - ); - }); - - it('testLogicalOperators', async () => { - const results = await randomCol - .pipeline() - .where( - orFunction( - andFunction(gt('rating', 4.5), eq('genre', 'Science Fiction')), - lt('published', 1900) - ) - ) - .select('title') - .sort(Field.of('title').ascending()) - .execute(); - expectResults( - results, - { title: 'Crime and Punishment' }, - { title: 'Dune' }, - { title: 'Pride and Prejudice' } - ); - }); - - it('testChecks', async () => { - const results = await randomCol - .pipeline() - .where(not(Field.of('rating').isNaN())) - .select( - Field.of('rating').eq(null).as('ratingIsNull'), - not(Field.of('rating').isNaN()).as('ratingIsNotNaN') - ) - .limit(1) - .execute(); - expectResults(results, { ratingIsNull: false, ratingIsNotNaN: true }); - }); - - it('testMapGet', async () => { - const results = await randomCol - .pipeline() - .select( - Field.of('awards').mapGet('hugo').as('hugoAward'), - Field.of('awards').mapGet('others').as('others'), - Field.of('title') - ) - .where(eq('hugoAward', true)) - .execute(); - expectResults( - results, - { - hugoAward: true, - title: "The Hitchhiker's Guide to the Galaxy", - others: { unknown: { year: 1980 } } - }, - { hugoAward: true, title: 'Dune', others: null } - ); - }); - - // it('testParent', async () => { - // const results = await randomCol - // .pipeline() - // .select( - // parent(randomCol.doc('chile').collection('subCollection').path).as( - // 'parent' - // ) - // ) - // .limit(1) - // .execute(); - // expect(results[0].data().parent.endsWith('/books')).to.be.true; - // }); - // - // it('testCollectionId', async () => { - // const results = await randomCol - // .pipeline() - // .select(collectionId(randomCol.doc('chile')).as('collectionId')) - // .limit(1) - // .execute(); - // expectResults(results, {collectionId: 'books'}); - // }); - - it('testDistanceFunctions', async () => { - const sourceVector = [0.1, 0.1]; - const targetVector = [0.5, 0.8]; - const results = await randomCol - .pipeline() - .select( - cosineDistance(Constant.vector(sourceVector), targetVector).as( - 'cosineDistance' - ), - dotProduct(Constant.vector(sourceVector), targetVector).as( - 'dotProductDistance' - ), - euclideanDistance(Constant.vector(sourceVector), targetVector).as( - 'euclideanDistance' - ) - ) - .limit(1) - .execute(); - - expectResults(results, { - cosineDistance: 0.02560880430538015, - dotProductDistance: 0.13, - euclideanDistance: 0.806225774829855 - }); - }); - - it('testNestedFields', async () => { - const results = await randomCol - .pipeline() - .where(eq('awards.hugo', true)) - .select('title', 'awards.hugo') - .execute(); - expectResults( - results, - { title: "The Hitchhiker's Guide to the Galaxy", 'awards.hugo': true }, - { title: 'Dune', 'awards.hugo': true } - ); - }); - - it('test mapGet with field name including . notation', async () => { - const results = await randomCol - .pipeline() - .where(eq('awards.hugo', true)) - .select( - 'title', - Field.of('nestedField.level.1'), - mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') - ) - .execute(); - expectResults( - results, - { - title: "The Hitchhiker's Guide to the Galaxy", - 'nestedField.level.`1`': null, - nested: true - }, - { title: 'Dune', 'nestedField.level.`1`': null, nested: null } - ); - }); - - it('supports internal serialization to proto', async () => { - const pipeline = firestore - .pipeline() - .collection('books') - .where(eq('awards.hugo', true)) - .select( - 'title', - Field.of('nestedField.level.1'), - mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') - ); - - const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); - expect(proto).not.to.be.null; - }); - - // TODO(pipeline) support converter - // it('pipeline converter works', async () => { - // interface AppModel {myTitle: string; myAuthor: string; myPublished: number} - // const converter: FirestorePipelineConverter = { - // fromFirestore(result: FirebaseFirestore.PipelineResult): AppModel { - // return { - // myTitle: result.data()!.title as string, - // myAuthor: result.data()!.author as string, - // myPublished: result.data()!.published as number, - // }; - // }, - // }; - // - // const results = await firestore - // .pipeline() - // .collection(randomCol.path) - // .sort(Field.of('published').ascending()) - // .limit(2) - // .withConverter(converter) - // .execute(); - // - // const objs = results.map(r => r.data()); - // expect(objs[0]).to.deep.equal({ - // myAuthor: 'Jane Austen', - // myPublished: 1813, - // myTitle: 'Pride and Prejudice', - // }); - // expect(objs[1]).to.deep.equal({ - // myAuthor: 'Fyodor Dostoevsky', - // myPublished: 1866, - // myTitle: 'Crime and Punishment', - // }); - // }); - describe('modular API', () => { - it('works when creating a pipeline from a Firestore instance', async () => { - const myPipeline = pipeline(firestore) - .collection(randomCol.path) - .where(lt(Field.of('published'), 1984)) - .aggregate({ - accumulators: [avgFunction('rating').as('avgRating')], - groups: ['genre'] - }) - .where(gt('avgRating', 4.3)) - .sort(Field.of('avgRating').descending()); - - const results = await execute(myPipeline); - - expectResults( - results, - { avgRating: 4.7, genre: 'Fantasy' }, - { avgRating: 4.5, genre: 'Romance' }, - { avgRating: 4.4, genre: 'Science Fiction' } - ); - }); - - it('works when creating a pipeline from a collection', async () => { - const myPipeline = pipeline(randomCol) - .where(lt(Field.of('published'), 1984)) - .aggregate({ - accumulators: [avgFunction('rating').as('avgRating')], - groups: ['genre'] - }) - .where(gt('avgRating', 4.3)) - .sort(Field.of('avgRating').descending()); - - const results = await execute(myPipeline); - - expectResults( - results, - { avgRating: 4.7, genre: 'Fantasy' }, - { avgRating: 4.5, genre: 'Romance' }, - { avgRating: 4.4, genre: 'Science Fiction' } - ); - }); - }); -}); diff --git a/packages/firestore/test/integration/api/transactions.test.ts b/packages/firestore/test/integration/api/transactions.test.ts index a4d30677a92..0decd1e5fca 100644 --- a/packages/firestore/test/integration/api/transactions.test.ts +++ b/packages/firestore/test/integration/api/transactions.test.ts @@ -593,6 +593,19 @@ apiDescribe('Database transactions', persistence => { } ); + it('runTransaction with empty message reject inside', () => { + return withTestDb(persistence, async db => { + try { + await runTransaction(db, () => { + return Promise.reject(); + }); + expect.fail('transaction should fail'); + } catch (err) { + expect(err).to.be.undefined; + } + }); + }); + describe('must return a promise:', () => { const noop = (): void => { /* -_- */ diff --git a/packages/firestore/test/unit/api/document_change.test.ts b/packages/firestore/test/unit/api/document_change.test.ts index 8ce40f599b8..faae8b4d4c8 100644 --- a/packages/firestore/test/unit/api/document_change.test.ts +++ b/packages/firestore/test/unit/api/document_change.test.ts @@ -18,8 +18,8 @@ import { expect } from 'chai'; import { Query } from '../../../src/api/reference'; +import { ExpUserDataWriter } from '../../../src/api/reference_impl'; import { QuerySnapshot } from '../../../src/api/snapshot'; -import { ExpUserDataWriter } from '../../../src/api/user_data_writer'; import { Query as InternalQuery } from '../../../src/core/query'; import { View } from '../../../src/core/view'; import { documentKeySet } from '../../../src/model/collections'; diff --git a/packages/firestore/test/unit/remote/serializer.helper.ts b/packages/firestore/test/unit/remote/serializer.helper.ts index 451f7ddf7ae..d523c8fab83 100644 --- a/packages/firestore/test/unit/remote/serializer.helper.ts +++ b/packages/firestore/test/unit/remote/serializer.helper.ts @@ -28,7 +28,7 @@ import { serverTimestamp, Timestamp } from '../../../src'; -import { ExpUserDataWriter } from '../../../src/api/user_data_writer'; +import { ExpUserDataWriter } from '../../../src/api/reference_impl'; import { DatabaseId } from '../../../src/core/database_info'; import { ArrayContainsAnyFilter, diff --git a/packages/firestore/test/unit/specs/spec_builder.ts b/packages/firestore/test/unit/specs/spec_builder.ts index afc6791dbd5..3e52c5873b9 100644 --- a/packages/firestore/test/unit/specs/spec_builder.ts +++ b/packages/firestore/test/unit/specs/spec_builder.ts @@ -16,7 +16,7 @@ */ import { IndexConfiguration } from '../../../src/api/index_configuration'; -import { ExpUserDataWriter } from '../../../src/api/user_data_writer'; +import { ExpUserDataWriter } from '../../../src/api/reference_impl'; import { ListenOptions, ListenerDataSource as Source diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index daa513edb68..51d2229b8a1 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -365,14 +365,8 @@ abstract class TestRunner { this.eventManager.onLastRemoteStoreUnlisten = triggerRemoteStoreUnlisten.bind(null, this.syncEngine); - this.persistence.setDatabaseDeletedListener(() => { - this.shutdown().catch(error => { - console.warn( - 'WARNING: this.shutdown() failed in callback ' + - 'specified to persistence.setDatabaseDeletedListener', - error - ); - }); + await this.persistence.setDatabaseDeletedListener(async () => { + await this.shutdown(); }); this.started = true; diff --git a/packages/firestore/test/util/api_helpers.ts b/packages/firestore/test/util/api_helpers.ts index dc66a70a85b..d248c9213b5 100644 --- a/packages/firestore/test/util/api_helpers.ts +++ b/packages/firestore/test/util/api_helpers.ts @@ -32,7 +32,7 @@ import { EmptyAppCheckTokenProvider, EmptyAuthCredentialsProvider } from '../../src/api/credentials'; -import { ExpUserDataWriter } from '../../src/api/user_data_writer'; +import { ExpUserDataWriter } from '../../src/api/reference_impl'; import { DatabaseId } from '../../src/core/database_info'; import { newQueryForPath, Query as InternalQuery } from '../../src/core/query'; import { diff --git a/packages/functions-compat/CHANGELOG.md b/packages/functions-compat/CHANGELOG.md index e2317dd8152..81ea4679642 100644 --- a/packages/functions-compat/CHANGELOG.md +++ b/packages/functions-compat/CHANGELOG.md @@ -1,5 +1,36 @@ # @firebase/functions-compat +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`5501791`](https://github.com/firebase/firebase-js-sdk/commit/5501791d0bd665c1c7d4fcd786053a46ceff208c)]: + - @firebase/functions@0.13.1 + +## 0.4.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/functions@0.13.0 + - @firebase/util@1.13.0 + +## 0.3.26 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/functions@0.12.9 + ## 0.3.25 ### Patch Changes diff --git a/packages/functions-compat/package.json b/packages/functions-compat/package.json index 24a8efb46d8..fd5b482bb66 100644 --- a/packages/functions-compat/package.json +++ b/packages/functions-compat/package.json @@ -1,23 +1,23 @@ { "name": "@firebase/functions-compat", - "version": "0.3.25", + "version": "0.4.1", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", "node": { "require": "./dist/index.cjs.js", - "import": "./dist/esm/index.esm2017.js" + "import": "./dist/esm/index.esm.js" }, "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/esm/index.esm2017.js" + "import": "./dist/esm/index.esm.js" }, - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -29,7 +29,7 @@ "@firebase/app-compat": "0.x" }, "devDependencies": { - "@firebase/app-compat": "0.4.1", + "@firebase/app-compat": "0.5.3", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", @@ -62,10 +62,10 @@ }, "typings": "dist/src/index.d.ts", "dependencies": { - "@firebase/component": "0.6.17", - "@firebase/functions": "0.12.8", + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.1", "@firebase/functions-types": "0.6.3", - "@firebase/util": "1.12.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "nyc": { @@ -75,6 +75,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/functions/CHANGELOG.md b/packages/functions/CHANGELOG.md index dcad8633b23..8b98d691f20 100644 --- a/packages/functions/CHANGELOG.md +++ b/packages/functions/CHANGELOG.md @@ -1,5 +1,33 @@ # @firebase/functions +## 0.13.1 + +### Patch Changes + +- [`5501791`](https://github.com/firebase/firebase-js-sdk/commit/5501791d0bd665c1c7d4fcd786053a46ceff208c) [#9204](https://github.com/firebase/firebase-js-sdk/pull/9204) - Fixed issue where Firebase Functions SDK caused CORS errors when connected to emulators in Firebase Studio + +## 0.13.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/util@1.13.0 + +## 0.12.9 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 0.12.8 ### Patch Changes diff --git a/packages/functions/package.json b/packages/functions/package.json index 6354ed4dadb..6099275f6cf 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -1,23 +1,23 @@ { "name": "@firebase/functions", - "version": "0.12.8", + "version": "0.13.1", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/functions-public.d.ts", "node": { - "import": "./dist/esm/index.esm2017.js", + "import": "./dist/esm/index.esm.js", "require": "./dist/index.cjs.js" }, "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/esm/index.esm2017.js" + "import": "./dist/esm/index.esm.js" }, - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -49,7 +49,7 @@ "@firebase/app": "0.x" }, "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", @@ -65,11 +65,11 @@ }, "typings": "dist/src/index.d.ts", "dependencies": { - "@firebase/component": "0.6.17", + "@firebase/component": "0.7.0", "@firebase/messaging-interop-types": "0.2.3", "@firebase/auth-interop-types": "0.2.4", "@firebase/app-check-interop-types": "0.3.3", - "@firebase/util": "1.12.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "nyc": { @@ -79,6 +79,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/functions/rollup.config.js b/packages/functions/rollup.config.js index 06899e7224c..797c263e3b8 100644 --- a/packages/functions/rollup.config.js +++ b/packages/functions/rollup.config.js @@ -49,7 +49,7 @@ const builds = [ external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ] }, @@ -63,7 +63,7 @@ const builds = [ external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ] } ]; diff --git a/packages/functions/src/callable.test.ts b/packages/functions/src/callable.test.ts index b969304c89e..724efc39c92 100644 --- a/packages/functions/src/callable.test.ts +++ b/packages/functions/src/callable.test.ts @@ -37,7 +37,11 @@ import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; import { makeFakeApp, createTestService } from '../test/utils'; -import { FunctionsService, httpsCallable } from './service'; +import { + FunctionsService, + httpsCallable, + httpsCallableFromURL +} from './service'; import { FUNCTIONS_TYPE } from './constants'; import { FunctionsError } from './error'; @@ -523,9 +527,136 @@ describe('Firebase Functions > Stream', () => { const [_, options] = mockFetch.firstCall.args; expect(options.headers['Authorization']).to.equal('Bearer auth-token'); expect(options.headers['Content-Type']).to.equal('application/json'); + expect(options.credentials).to.equal(undefined); expect(options.headers['Accept']).to.equal('text/event-stream'); }); + it('calls cloud workstations with credentials', async () => { + const authMock: FirebaseAuthInternal = { + getToken: async () => ({ accessToken: 'auth-token' }) + } as unknown as FirebaseAuthInternal; + const authProvider = new Provider( + 'auth-internal', + new ComponentContainer('test') + ); + authProvider.setComponent( + new Component('auth-internal', () => authMock, ComponentType.PRIVATE) + ); + const appCheckMock: FirebaseAppCheckInternal = { + getToken: async () => ({ token: 'app-check-token' }) + } as unknown as FirebaseAppCheckInternal; + const appCheckProvider = new Provider( + 'app-check-internal', + new ComponentContainer('test') + ); + appCheckProvider.setComponent( + new Component( + 'app-check-internal', + () => appCheckMock, + ComponentType.PRIVATE + ) + ); + + const functions = createTestService( + app, + region, + authProvider, + undefined, + appCheckProvider + ); + functions.emulatorOrigin = 'test.cloudworkstations.dev'; + const mockFetch = sinon.stub(functions, 'fetchImpl' as any); + + const mockResponse = new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('data: {"result":"Success"}\n') + ); + controller.close(); + } + }); + + mockFetch.resolves({ + body: mockResponse, + headers: new Headers({ 'Content-Type': 'text/event-stream' }), + status: 200, + statusText: 'OK' + } as Response); + + const func = httpsCallable, string, string>( + functions, + 'stream' + ); + await func.stream({}); + + expect(mockFetch.calledOnce).to.be.true; + const [_, options] = mockFetch.firstCall.args; + expect(options.credentials).to.equal('include'); + }); + + it('calls streamFromURL cloud workstations with credentials', async () => { + const authMock: FirebaseAuthInternal = { + getToken: async () => ({ accessToken: 'auth-token' }) + } as unknown as FirebaseAuthInternal; + const authProvider = new Provider( + 'auth-internal', + new ComponentContainer('test') + ); + authProvider.setComponent( + new Component('auth-internal', () => authMock, ComponentType.PRIVATE) + ); + const appCheckMock: FirebaseAppCheckInternal = { + getToken: async () => ({ token: 'app-check-token' }) + } as unknown as FirebaseAppCheckInternal; + const appCheckProvider = new Provider( + 'app-check-internal', + new ComponentContainer('test') + ); + appCheckProvider.setComponent( + new Component( + 'app-check-internal', + () => appCheckMock, + ComponentType.PRIVATE + ) + ); + + const functions = createTestService( + app, + region, + authProvider, + undefined, + appCheckProvider + ); + functions.emulatorOrigin = 'test.cloudworkstations.dev'; + const mockFetch = sinon.stub(functions, 'fetchImpl' as any); + + const mockResponse = new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('data: {"result":"Success"}\n') + ); + controller.close(); + } + }); + + mockFetch.resolves({ + body: mockResponse, + headers: new Headers({ 'Content-Type': 'text/event-stream' }), + status: 200, + statusText: 'OK' + } as Response); + + const func = httpsCallableFromURL, string, string>( + functions, + 'stream' + ); + await func.stream({}); + + expect(mockFetch.calledOnce).to.be.true; + const [_, options] = mockFetch.firstCall.args; + expect(options.credentials).to.equal('include'); + }); + it('aborts during initial fetch', async () => { const controller = new AbortController(); diff --git a/packages/functions/src/config.ts b/packages/functions/src/config.ts index 8dfadb52b54..ab596a1cbc0 100644 --- a/packages/functions/src/config.ts +++ b/packages/functions/src/config.ts @@ -65,6 +65,6 @@ export function registerFunctions(variant?: string): void { ); registerVersion(name, version, variant); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/functions/src/service.ts b/packages/functions/src/service.ts index 57504a4c7a4..6e2eddda3a2 100644 --- a/packages/functions/src/service.ts +++ b/packages/functions/src/service.ts @@ -185,7 +185,7 @@ export function connectFunctionsEmulator( }://${host}:${port}`; // Workaround to get cookies in Firebase Studio if (useSsl) { - void pingServer(functionsInstance.emulatorOrigin); + void pingServer(functionsInstance.emulatorOrigin + '/backends'); updateEmulatorBanner('Functions', true); } } @@ -245,18 +245,29 @@ export function httpsCallableFromURL< return callable as HttpsCallable; } +function getCredentials( + functionsInstance: FunctionsService +): 'include' | undefined { + return functionsInstance.emulatorOrigin && + isCloudWorkstation(functionsInstance.emulatorOrigin) + ? 'include' + : undefined; +} + /** * Does an HTTP POST and returns the completed response. * @param url The url to post to. * @param body The JSON body of the post. * @param headers The HTTP headers to include in the request. + * @param functionsInstance functions instance that is calling postJSON * @return A Promise that will succeed when the request finishes. */ async function postJSON( url: string, body: unknown, headers: { [key: string]: string }, - fetchImpl: typeof fetch + fetchImpl: typeof fetch, + functionsInstance: FunctionsService ): Promise { headers['Content-Type'] = 'application/json'; @@ -265,7 +276,8 @@ async function postJSON( response = await fetchImpl(url, { method: 'POST', body: JSON.stringify(body), - headers + headers, + credentials: getCredentials(functionsInstance) }); } catch (e) { // This could be an unhandled error on the backend, or it could be a @@ -353,7 +365,13 @@ async function callAtURL( const failAfterHandle = failAfter(timeout); const response = await Promise.race([ - postJSON(url, body, headers, functionsInstance.fetchImpl), + postJSON( + url, + body, + headers, + functionsInstance.fetchImpl, + functionsInstance + ), failAfterHandle.promise, functionsInstance.cancelAllRequests ]); @@ -439,7 +457,8 @@ async function streamAtURL( method: 'POST', body: JSON.stringify(body), headers, - signal: options?.signal + signal: options?.signal, + credentials: getCredentials(functionsInstance) }); } catch (e) { if (e instanceof Error && e.name === 'AbortError') { diff --git a/packages/installations-compat/CHANGELOG.md b/packages/installations-compat/CHANGELOG.md index 213a1ba641e..d9e0fad1ef3 100644 --- a/packages/installations-compat/CHANGELOG.md +++ b/packages/installations-compat/CHANGELOG.md @@ -1,5 +1,25 @@ # @firebase/installations-compat +## 0.2.19 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/installations@0.6.19 + - @firebase/component@0.7.0 + - @firebase/util@1.13.0 + +## 0.2.18 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/installations@0.6.18 + ## 0.2.17 ### Patch Changes diff --git a/packages/installations-compat/package.json b/packages/installations-compat/package.json index cb5f6f730a6..058f690f237 100644 --- a/packages/installations-compat/package.json +++ b/packages/installations-compat/package.json @@ -1,15 +1,15 @@ { "name": "@firebase/installations-compat", - "version": "0.2.17", + "version": "0.2.19", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "module": "dist/esm/index.esm2017.js", - "browser": "dist/esm/index.esm2017.js", + "module": "dist/esm/index.esm.js", + "browser": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -44,7 +44,7 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "@firebase/app-compat": "0.4.1", + "@firebase/app-compat": "0.5.3", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", @@ -57,10 +57,10 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/installations": "0.6.17", + "@firebase/installations": "0.6.19", "@firebase/installations-types": "0.5.3", - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" } } diff --git a/packages/installations/CHANGELOG.md b/packages/installations/CHANGELOG.md index bb0e09f277f..39cbde19a17 100644 --- a/packages/installations/CHANGELOG.md +++ b/packages/installations/CHANGELOG.md @@ -1,5 +1,23 @@ # @firebase/installations +## 0.6.19 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/util@1.13.0 + +## 0.6.18 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 0.6.17 ### Patch Changes diff --git a/packages/installations/package.json b/packages/installations/package.json index a1d89e48321..a1b5db5bdce 100644 --- a/packages/installations/package.json +++ b/packages/installations/package.json @@ -1,15 +1,15 @@ { "name": "@firebase/installations", - "version": "0.6.17", + "version": "0.6.19", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "module": "dist/esm/index.esm2017.js", - "browser": "dist/esm/index.esm2017.js", + "module": "dist/esm/index.esm.js", + "browser": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/installations-public.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -49,7 +49,7 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", @@ -62,8 +62,8 @@ "@firebase/app": "0.x" }, "dependencies": { - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "idb": "7.1.1", "tslib": "^2.1.0" } diff --git a/packages/installations/rollup.config.js b/packages/installations/rollup.config.js index bf2b61f73f0..694a1383787 100644 --- a/packages/installations/rollup.config.js +++ b/packages/installations/rollup.config.js @@ -46,7 +46,7 @@ const esmBuild = { external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ] }; @@ -61,7 +61,7 @@ const cjsBuild = { external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ] }; diff --git a/packages/installations/src/index.ts b/packages/installations/src/index.ts index 5a70ef10846..d56056ac189 100644 --- a/packages/installations/src/index.ts +++ b/packages/installations/src/index.ts @@ -31,5 +31,5 @@ export * from './interfaces/public-types'; registerInstallations(); registerVersion(name, version); -// BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation +// BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index 502b8d93945..78b740f4c6c 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -1,5 +1,15 @@ # @firebase/logger +## 0.5.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + ## 0.4.4 ### Patch Changes diff --git a/packages/logger/package.json b/packages/logger/package.json index f8661d3fdf7..f44fa21d524 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,15 +1,15 @@ { "name": "@firebase/logger", - "version": "0.4.4", + "version": "0.5.0", "description": "A logger package for use in the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "module": "dist/esm/index.esm2017.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -55,6 +55,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/messaging-compat/CHANGELOG.md b/packages/messaging-compat/CHANGELOG.md index b150f2f2939..98946c9d228 100644 --- a/packages/messaging-compat/CHANGELOG.md +++ b/packages/messaging-compat/CHANGELOG.md @@ -1,5 +1,25 @@ # @firebase/messaging-compat +## 0.2.23 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/messaging@0.12.23 + - @firebase/util@1.13.0 + +## 0.2.22 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/messaging@0.12.22 + ## 0.2.21 ### Patch Changes diff --git a/packages/messaging-compat/package.json b/packages/messaging-compat/package.json index c536cbef067..643e1bd73e9 100644 --- a/packages/messaging-compat/package.json +++ b/packages/messaging-compat/package.json @@ -1,17 +1,17 @@ { "name": "@firebase/messaging-compat", - "version": "0.2.21", + "version": "0.2.23", "license": "Apache-2.0", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -38,13 +38,13 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/messaging": "0.12.21", - "@firebase/component": "0.6.17", - "@firebase/util": "1.12.0", + "@firebase/messaging": "0.12.23", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.4.1", + "@firebase/app-compat": "0.5.3", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", "ts-essentials": "9.4.2", diff --git a/packages/messaging-compat/tsconfig.json b/packages/messaging-compat/tsconfig.json index 90f9c26f657..4ec5372577b 100644 --- a/packages/messaging-compat/tsconfig.json +++ b/packages/messaging-compat/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "dist", "noUnusedLocals": true, - "lib": ["dom", "es2017"], + "lib": ["dom", "es2020"], "downlevelIteration": true }, "exclude": ["dist/**/*"] diff --git a/packages/messaging/CHANGELOG.md b/packages/messaging/CHANGELOG.md index 8df2d983835..6fe74948992 100644 --- a/packages/messaging/CHANGELOG.md +++ b/packages/messaging/CHANGELOG.md @@ -1,5 +1,25 @@ # @firebase/messaging +## 0.12.23 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/installations@0.6.19 + - @firebase/component@0.7.0 + - @firebase/util@1.13.0 + +## 0.12.22 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/installations@0.6.18 + ## 0.12.21 ### Patch Changes diff --git a/packages/messaging/package.json b/packages/messaging/package.json index 4419ad3acf4..4d475ebadfa 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -1,24 +1,24 @@ { "name": "@firebase/messaging", - "version": "0.12.21", + "version": "0.12.23", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", - "sw": "dist/esm/index.sw.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", + "sw": "dist/esm/index.sw.esm.js", "sw-main": "dist/index.sw.cjs", "exports": { ".": { "types": "./dist/index-public.d.ts", "require": "./dist/index.cjs.js", - "module": "./dist/esm/index.esm2017.js", - "default": "./dist/esm/index.esm2017.js" + "module": "./dist/esm/index.esm.js", + "default": "./dist/esm/index.esm.js" }, "./sw": { "types": "./dist/sw/index-public.d.ts", "require": "./dist/index.sw.cjs", - "default": "./dist/esm/index.sw.esm2017.js" + "default": "./dist/esm/index.sw.esm.js" }, "./package.json": "./package.json" }, @@ -52,15 +52,15 @@ "@firebase/app": "0.x" }, "dependencies": { - "@firebase/installations": "0.6.17", + "@firebase/installations": "0.6.19", "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "idb": "7.1.1", "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/messaging/rollup.config.js b/packages/messaging/rollup.config.js index 9342be135f2..0c51c683294 100644 --- a/packages/messaging/rollup.config.js +++ b/packages/messaging/rollup.config.js @@ -48,7 +48,7 @@ const esmBuilds = [ }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) @@ -76,7 +76,7 @@ const cjsBuilds = [ }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) }, @@ -91,7 +91,7 @@ const cjsBuilds = [ output: { file: pkg['sw-main'], format: 'cjs', sourcemap: true }, plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } diff --git a/packages/messaging/src/helpers/register.ts b/packages/messaging/src/helpers/register.ts index ef28cd2edad..60f1dd0fe8c 100644 --- a/packages/messaging/src/helpers/register.ts +++ b/packages/messaging/src/helpers/register.ts @@ -104,7 +104,7 @@ export function registerMessagingInWindow(): void { ); registerVersion(name, version); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/messaging/src/testing/fakes/firebase-dependencies.ts b/packages/messaging/src/testing/fakes/firebase-dependencies.ts index 8fd3b219f33..ccb40f276ed 100644 --- a/packages/messaging/src/testing/fakes/firebase-dependencies.ts +++ b/packages/messaging/src/testing/fakes/firebase-dependencies.ts @@ -68,7 +68,8 @@ export function getFakeInstallations(): _FirebaseInstallationsInternal { export function getFakeAnalyticsProvider(): Provider { const analytics: FirebaseAnalyticsInternal = { - logEvent() {} + logEvent() {}, + setUserProperties() {} }; return { diff --git a/packages/messaging/sw/package.json b/packages/messaging/sw/package.json index 472e37684bc..9a076664821 100644 --- a/packages/messaging/sw/package.json +++ b/packages/messaging/sw/package.json @@ -3,6 +3,6 @@ "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "../dist/index.sw.cjs", - "module": "../dist/esm/index.sw.esm2017.js", + "module": "../dist/esm/index.sw.esm.js", "typings": "../dist/src/index.sw.d.ts" } diff --git a/packages/messaging/tsconfig.json b/packages/messaging/tsconfig.json index 90f9c26f657..4ec5372577b 100644 --- a/packages/messaging/tsconfig.json +++ b/packages/messaging/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "dist", "noUnusedLocals": true, - "lib": ["dom", "es2017"], + "lib": ["dom", "es2020"], "downlevelIteration": true }, "exclude": ["dist/**/*"] diff --git a/packages/performance-compat/CHANGELOG.md b/packages/performance-compat/CHANGELOG.md index 11b9eed3d2f..032e5fd54a7 100644 --- a/packages/performance-compat/CHANGELOG.md +++ b/packages/performance-compat/CHANGELOG.md @@ -1,5 +1,33 @@ # @firebase/performance-compat +## 0.2.22 + +### Patch Changes + +- Updated dependencies [[`a4897a6`](https://github.com/firebase/firebase-js-sdk/commit/a4897a621e99f270ddf6821d587fcddd3a0c5cd1)]: + - @firebase/performance@0.7.9 + +## 0.2.21 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`3d44792`](https://github.com/firebase/firebase-js-sdk/commit/3d44792f14f3df265162d06e2acdf3cad0c2ef86), [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/performance@0.7.8 + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.2.20 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/performance@0.7.7 + ## 0.2.19 ### Patch Changes diff --git a/packages/performance-compat/package.json b/packages/performance-compat/package.json index 0e84088a37d..32c041916e2 100644 --- a/packages/performance-compat/package.json +++ b/packages/performance-compat/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/performance-compat", - "version": "0.2.19", + "version": "0.2.22", "description": "The compatibility package of Firebase Performance", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -38,11 +38,11 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/performance": "0.7.6", + "@firebase/performance": "0.7.9", "@firebase/performance-types": "0.2.3", - "@firebase/util": "1.12.0", - "@firebase/logger": "0.4.4", - "@firebase/component": "0.6.17", + "@firebase/util": "1.13.0", + "@firebase/logger": "0.5.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" }, "devDependencies": { @@ -51,7 +51,7 @@ "rollup-plugin-replace": "2.2.0", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4", - "@firebase/app-compat": "0.4.1" + "@firebase/app-compat": "0.5.3" }, "repository": { "directory": "packages/performance-compat", diff --git a/packages/performance-compat/rollup.config.js b/packages/performance-compat/rollup.config.js index e52495c401e..b96fe548483 100644 --- a/packages/performance-compat/rollup.config.js +++ b/packages/performance-compat/rollup.config.js @@ -30,7 +30,7 @@ const buildPlugins = [ typescript, tsconfigOverride: { compilerOptions: { - target: 'es2017' + target: 'es2020' } } }), diff --git a/packages/performance/CHANGELOG.md b/packages/performance/CHANGELOG.md index ce6d7e391e5..e55f9b69e07 100644 --- a/packages/performance/CHANGELOG.md +++ b/packages/performance/CHANGELOG.md @@ -1,5 +1,34 @@ # @firebase/performance +## 0.7.9 + +### Patch Changes + +- [`a4897a6`](https://github.com/firebase/firebase-js-sdk/commit/a4897a621e99f270ddf6821d587fcddd3a0c5cd1) [#9178](https://github.com/firebase/firebase-js-sdk/pull/9178) (fixes [#9136](https://github.com/firebase/firebase-js-sdk/issues/9136)) - Fixed errors thrown when capturing long target element names for the out-of-the-box metrics. + +## 0.7.8 + +### Patch Changes + +- [`3d44792`](https://github.com/firebase/firebase-js-sdk/commit/3d44792f14f3df265162d06e2acdf3cad0c2ef86) [#9120](https://github.com/firebase/firebase-js-sdk/pull/9120) (fixes [#9067](https://github.com/firebase/firebase-js-sdk/issues/9067)) - Fix bug where events are not sent if they exceed sendBeacon payload limit + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/installations@0.6.19 + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.7.7 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/installations@0.6.18 + ## 0.7.6 ### Patch Changes diff --git a/packages/performance/package.json b/packages/performance/package.json index 27ff0073509..cfbd414c0a4 100644 --- a/packages/performance/package.json +++ b/packages/performance/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/performance", - "version": "0.7.6", + "version": "0.7.9", "description": "Firebase performance for web", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -38,16 +38,16 @@ "@firebase/app": "0.x" }, "dependencies": { - "@firebase/logger": "0.4.4", - "@firebase/installations": "0.6.17", - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/logger": "0.5.0", + "@firebase/installations": "0.6.19", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0", "web-vitals": "^4.2.4" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/performance/rollup.config.js b/packages/performance/rollup.config.js index 006dcf54337..ef30405c02c 100644 --- a/packages/performance/rollup.config.js +++ b/packages/performance/rollup.config.js @@ -44,7 +44,7 @@ const esmBuild = { external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ] }; @@ -59,7 +59,7 @@ const cjsBuild = { external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ] }; diff --git a/packages/performance/src/index.ts b/packages/performance/src/index.ts index 4ccb499b368..7ccc73ee6e9 100644 --- a/packages/performance/src/index.ts +++ b/packages/performance/src/index.ts @@ -137,7 +137,7 @@ function registerPerformance(): void { new Component('performance', factory, ComponentType.PUBLIC) ); registerVersion(name, version); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/performance/src/resources/trace.test.ts b/packages/performance/src/resources/trace.test.ts index 5742a6815b5..92deef7e0a1 100644 --- a/packages/performance/src/resources/trace.test.ts +++ b/packages/performance/src/resources/trace.test.ts @@ -306,4 +306,37 @@ describe('Firebase Performance > trace', () => { expect(trace.getAttribute('stage')).to.equal('beginning'); }); }); + + describe('#addWebVitalMetric', () => { + it('has correctly scaled metric', () => { + Trace.addWebVitalMetric(trace, 'metric', 'attributeName', { + value: 0.5, + elementAttribution: 'test' + }); + + expect(trace.getMetric('metric') === 500); + }); + + it('has correct attribute', () => { + Trace.addWebVitalMetric(trace, 'metric', 'attributeName', { + value: 0.5, + elementAttribution: 'test' + }); + + expect(trace.getAttribute('attributeName') === 'test'); + }); + + it('correctly truncates long attribute names', () => { + Trace.addWebVitalMetric(trace, 'metric', 'attributeName', { + value: 0.5, + elementAttribution: + 'html>body>main>p>button.my_button_class.really_long_class_name_that_is_above_100_characters.another_long_class_name' + }); + + expect( + trace.getAttribute('attributeName') === + 'html>body>main>p>button.my_button_class.really_long_class_name_that_is_above_100_characters.another_' + ); + }); + }); }); diff --git a/packages/performance/src/resources/trace.ts b/packages/performance/src/resources/trace.ts index d6657f14ba6..ecc94472929 100644 --- a/packages/performance/src/resources/trace.ts +++ b/packages/performance/src/resources/trace.ts @@ -34,6 +34,7 @@ import { Api } from '../services/api_service'; import { logTrace, flushLogs } from '../services/perf_logger'; import { ERROR_FACTORY, ErrorCode } from '../utils/errors'; import { + MAX_ATTRIBUTE_VALUE_LENGTH, isValidCustomAttributeName, isValidCustomAttributeValue } from '../utils/attributes_utils'; @@ -382,7 +383,14 @@ export class Trace implements PerformanceTrace { if (metric) { trace.putMetric(metricKey, Math.floor(metric.value * 1000)); if (metric.elementAttribution) { - trace.putAttribute(attributeKey, metric.elementAttribution); + if (metric.elementAttribution.length > MAX_ATTRIBUTE_VALUE_LENGTH) { + trace.putAttribute( + attributeKey, + metric.elementAttribution.substring(0, MAX_ATTRIBUTE_VALUE_LENGTH) + ); + } else { + trace.putAttribute(attributeKey, metric.elementAttribution); + } } } } diff --git a/packages/performance/src/services/remote_config_service.test.ts b/packages/performance/src/services/remote_config_service.test.ts index 78f1b1b4462..49b8893140f 100644 --- a/packages/performance/src/services/remote_config_service.test.ts +++ b/packages/performance/src/services/remote_config_service.test.ts @@ -40,7 +40,8 @@ describe('Performance Monitoring > remote_config_service', () => { "fpr_log_endpoint_url":"https://firebaselogging.test.com",\ "fpr_log_transport_key":"pseudo-transport-key",\ "fpr_log_source":"2","fpr_vc_network_request_sampling_rate":"0.250000",\ - "fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000"},\ + "fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000", + "fpr_log_max_flush_size":"10"},\ "state":"UPDATE"}`; const PROJECT_ID = 'project1'; const APP_ID = '1:23r:web:fewq'; @@ -80,6 +81,7 @@ describe('Performance Monitoring > remote_config_service', () => { settingsService.loggingEnabled = false; settingsService.networkRequestsSamplingRate = 1; settingsService.tracesSamplingRate = 1; + settingsService.logMaxFlushSize = 40; } // parameterized beforeEach. Should be called at beginning of each test. @@ -150,6 +152,7 @@ describe('Performance Monitoring > remote_config_service', () => { expect(SettingsService.getInstance().tracesSamplingRate).to.equal( TRACE_SAMPLING_RATE ); + expect(SettingsService.getInstance().logMaxFlushSize).to.equal(10); }); it('does not call remote config if a valid config is in local storage', async () => { @@ -190,6 +193,7 @@ describe('Performance Monitoring > remote_config_service', () => { expect(SettingsService.getInstance().tracesSamplingRate).to.equal( TRACE_SAMPLING_RATE ); + expect(SettingsService.getInstance().logMaxFlushSize).to.equal(10); }); it('does not change the default config if call to RC fails', async () => { @@ -207,6 +211,7 @@ describe('Performance Monitoring > remote_config_service', () => { await getConfig(performanceController, IID); expect(SettingsService.getInstance().loggingEnabled).to.equal(false); + expect(SettingsService.getInstance().logMaxFlushSize).to.equal(40); }); it('uses secondary configs if the response does not have all the fields', async () => { diff --git a/packages/performance/src/services/remote_config_service.ts b/packages/performance/src/services/remote_config_service.ts index 13787e2b693..1641b3b953b 100644 --- a/packages/performance/src/services/remote_config_service.ts +++ b/packages/performance/src/services/remote_config_service.ts @@ -38,6 +38,7 @@ interface SecondaryConfig { transportKey?: string; tracesSamplingRate?: number; networkRequestsSamplingRate?: number; + logMaxFlushSize?: number; } // These values will be used if the remote config object is successfully @@ -56,6 +57,7 @@ interface RemoteConfigTemplate { fpr_vc_network_request_sampling_rate?: string; fpr_vc_trace_sampling_rate?: string; fpr_vc_session_sampling_rate?: string; + fpr_log_max_flush_size?: string; } /* eslint-enable camelcase */ @@ -221,6 +223,14 @@ function processConfig( settingsServiceInstance.tracesSamplingRate = DEFAULT_CONFIGS.tracesSamplingRate; } + + if (entries.fpr_log_max_flush_size) { + settingsServiceInstance.logMaxFlushSize = Number( + entries.fpr_log_max_flush_size + ); + } else if (DEFAULT_CONFIGS.logMaxFlushSize) { + settingsServiceInstance.logMaxFlushSize = DEFAULT_CONFIGS.logMaxFlushSize; + } // Set the per session trace and network logging flags. settingsServiceInstance.logTraceAfterSampling = shouldLogAfterSampling( settingsServiceInstance.tracesSamplingRate diff --git a/packages/performance/src/services/settings_service.ts b/packages/performance/src/services/settings_service.ts index 83e08bd53d5..dbaed32a537 100644 --- a/packages/performance/src/services/settings_service.ts +++ b/packages/performance/src/services/settings_service.ts @@ -54,6 +54,10 @@ export class SettingsService { // TTL of config retrieved from remote config in hours. configTimeToLive = 12; + // The max number of events to send during a flush. This number is kept low to since Chrome has a + // shared payload limit for all sendBeacon calls in the same nav context. + logMaxFlushSize = 40; + getFlTransportFullUrl(): string { return this.flTransportEndpointUrl.concat('?key=', this.transportKey); } diff --git a/packages/performance/src/services/transport_service.test.ts b/packages/performance/src/services/transport_service.test.ts index 124ce1f415b..4f46205958d 100644 --- a/packages/performance/src/services/transport_service.test.ts +++ b/packages/performance/src/services/transport_service.test.ts @@ -21,7 +21,8 @@ import sinonChai from 'sinon-chai'; import { transportHandler, setupTransportService, - resetTransportService + resetTransportService, + flushQueuedEvents } from './transport_service'; import { SettingsService } from './settings_service'; @@ -88,14 +89,15 @@ describe('Firebase Performance > transport_service', () => { expect(fetchStub).to.not.have.been.called; }); - it('sends up to the maximum event limit in one request', async () => { + it('sends up to the maximum event limit in one request if payload is under 64 KB', async () => { // Arrange const setting = SettingsService.getInstance(); const flTransportFullUrl = setting.flTransportEndpointUrl + '?key=' + setting.transportKey; // Act - // Generate 1020 events, which should be dispatched in two batches (1000 events and 20 events). + // Generate 1020 events with small payloads, which should be dispatched in two batches + // (1000 events and 20 events). for (let i = 0; i < 1020; i++) { testTransportHandler('event' + i); } @@ -134,6 +136,58 @@ describe('Firebase Performance > transport_service', () => { expect(fetchStub).to.not.have.been.called; }); + it('sends fetch if payload is above 64 KB', async () => { + // Arrange + const setting = SettingsService.getInstance(); + const flTransportFullUrl = + setting.flTransportEndpointUrl + '?key=' + setting.transportKey; + fetchStub.resolves( + new Response('{}', { + status: 200, + headers: { 'Content-type': 'application/json' } + }) + ); + + // Act + // Generate 1020 events with a large payload. The total size of the payload will be > 65 KB + const payload = 'a'.repeat(300); + for (let i = 0; i < 1020; i++) { + testTransportHandler(payload + i); + } + // Wait for first and second event dispatch to happen. + clock.tick(INITIAL_SEND_TIME_DELAY_MS); + // This is to resolve the floating promise chain in transport service. + await Promise.resolve().then().then().then(); + clock.tick(DEFAULT_SEND_INTERVAL_MS); + + // Assert + // Expects the first logRequest which contains first 1000 events. + const firstLogRequest = generateLogRequest('5501'); + for (let i = 0; i < MAX_EVENT_COUNT_PER_REQUEST; i++) { + firstLogRequest['log_event'].push({ + 'source_extension_json_proto3': payload + i, + 'event_time_ms': '1' + }); + } + expect(fetchStub).calledWith(flTransportFullUrl, { + method: 'POST', + body: JSON.stringify(firstLogRequest) + }); + // Expects the second logRequest which contains remaining 20 events; + const secondLogRequest = generateLogRequest('15501'); + for (let i = 0; i < 20; i++) { + secondLogRequest['log_event'].push({ + 'source_extension_json_proto3': + payload + (MAX_EVENT_COUNT_PER_REQUEST + i), + 'event_time_ms': '1' + }); + } + expect(sendBeaconStub).calledWith( + flTransportFullUrl, + JSON.stringify(secondLogRequest) + ); + }); + it('falls back to fetch if sendBeacon fails.', async () => { sendBeaconStub.returns(false); fetchStub.resolves( @@ -147,6 +201,98 @@ describe('Firebase Performance > transport_service', () => { expect(fetchStub).to.have.been.calledOnce; }); + it('flushes the queue with multiple sendBeacons in batches of 40', async () => { + // Arrange + const setting = SettingsService.getInstance(); + const flTransportFullUrl = + setting.flTransportEndpointUrl + '?key=' + setting.transportKey; + fetchStub.resolves( + new Response('{}', { + status: 200, + headers: { 'Content-type': 'application/json' } + }) + ); + + const payload = 'a'.repeat(300); + // Act + // Generate 80 events + for (let i = 0; i < 80; i++) { + testTransportHandler(payload + i); + } + + flushQueuedEvents(); + + // Assert + const firstLogRequest = generateLogRequest('1'); + const secondLogRequest = generateLogRequest('1'); + for (let i = 0; i < 40; i++) { + firstLogRequest['log_event'].push({ + 'source_extension_json_proto3': payload + (i + 40), + 'event_time_ms': '1' + }); + secondLogRequest['log_event'].push({ + 'source_extension_json_proto3': payload + i, + 'event_time_ms': '1' + }); + } + expect(sendBeaconStub).calledWith( + flTransportFullUrl, + JSON.stringify(firstLogRequest) + ); + expect(sendBeaconStub).calledWith( + flTransportFullUrl, + JSON.stringify(secondLogRequest) + ); + expect(fetchStub).to.not.have.been.called; + }); + + it('flushes the queue with fetch for sendBeacons that failed', async () => { + // Arrange + const setting = SettingsService.getInstance(); + const flTransportFullUrl = + setting.flTransportEndpointUrl + '?key=' + setting.transportKey; + fetchStub.resolves( + new Response('{}', { + status: 200, + headers: { 'Content-type': 'application/json' } + }) + ); + + const payload = 'a'.repeat(300); + // Act + // Generate 80 events + for (let i = 0; i < 80; i++) { + testTransportHandler(payload + i); + } + sendBeaconStub.onCall(0).returns(true); + sendBeaconStub.onCall(1).returns(false); + flushQueuedEvents(); + + // Assert + const firstLogRequest = generateLogRequest('1'); + const secondLogRequest = generateLogRequest('1'); + for (let i = 40; i < 80; i++) { + firstLogRequest['log_event'].push({ + 'source_extension_json_proto3': payload + i, + 'event_time_ms': '1' + }); + } + for (let i = 0; i < 40; i++) { + secondLogRequest['log_event'].push({ + 'source_extension_json_proto3': payload + i, + 'event_time_ms': '1' + }); + } + expect(sendBeaconStub).calledWith( + flTransportFullUrl, + JSON.stringify(firstLogRequest) + ); + expect(fetchStub).calledWith(flTransportFullUrl, { + method: 'POST', + body: JSON.stringify(secondLogRequest) + }); + }); + function generateLogRequest(requestTimeMs: string): any { return { 'request_time_ms': requestTimeMs, diff --git a/packages/performance/src/services/transport_service.ts b/packages/performance/src/services/transport_service.ts index 8577fd3a128..46c9930210a 100644 --- a/packages/performance/src/services/transport_service.ts +++ b/packages/performance/src/services/transport_service.ts @@ -24,6 +24,11 @@ const INITIAL_SEND_TIME_DELAY_MS = 5.5 * 1000; const MAX_EVENT_COUNT_PER_REQUEST = 1000; const DEFAULT_REMAINING_TRIES = 3; +// Most browsers have a max payload of 64KB for sendbeacon/keep alive payload. +const MAX_SEND_BEACON_PAYLOAD_SIZE = 65536; + +const TEXT_ENCODER = new TextEncoder(); + let remainingTries = DEFAULT_REMAINING_TRIES; interface BatchEvent { @@ -90,14 +95,31 @@ function dispatchQueueEvents(): void { // for next attempt. const staged = queue.splice(0, MAX_EVENT_COUNT_PER_REQUEST); + const data = buildPayload(staged); + + postToFlEndpoint(data) + .then(() => { + remainingTries = DEFAULT_REMAINING_TRIES; + }) + .catch(() => { + // If the request fails for some reason, add the events that were attempted + // back to the primary queue to retry later. + queue = [...staged, ...queue]; + remainingTries--; + consoleLogger.info(`Tries left: ${remainingTries}.`); + processQueue(DEFAULT_SEND_INTERVAL_MS); + }); +} + +function buildPayload(events: BatchEvent[]): string { /* eslint-disable camelcase */ // We will pass the JSON serialized event to the backend. - const log_event: Log[] = staged.map(evt => ({ + const log_event: Log[] = events.map(evt => ({ source_extension_json_proto3: evt.message, event_time_ms: String(evt.eventTime) })); - const data: TransportBatchLogFormat = { + const transportBatchLog: TransportBatchLogFormat = { request_time_ms: String(Date.now()), client_info: { client_type: 1, // 1 is JS @@ -108,32 +130,27 @@ function dispatchQueueEvents(): void { }; /* eslint-enable camelcase */ - postToFlEndpoint(data) - .then(() => { - remainingTries = DEFAULT_REMAINING_TRIES; - }) - .catch(() => { - // If the request fails for some reason, add the events that were attempted - // back to the primary queue to retry later. - queue = [...staged, ...queue]; - remainingTries--; - consoleLogger.info(`Tries left: ${remainingTries}.`); - processQueue(DEFAULT_SEND_INTERVAL_MS); - }); + return JSON.stringify(transportBatchLog); } -function postToFlEndpoint(data: TransportBatchLogFormat): Promise { +/** Sends to Firelog. Atempts to use sendBeacon otherwsise uses fetch. */ +function postToFlEndpoint(body: string): Promise { const flTransportFullUrl = SettingsService.getInstance().getFlTransportFullUrl(); - const body = JSON.stringify(data); - - return navigator.sendBeacon && navigator.sendBeacon(flTransportFullUrl, body) - ? Promise.resolve() - : fetch(flTransportFullUrl, { - method: 'POST', - body, - keepalive: true - }).then(); + const size = TEXT_ENCODER.encode(body).length; + + if ( + size <= MAX_SEND_BEACON_PAYLOAD_SIZE && + navigator.sendBeacon && + navigator.sendBeacon(flTransportFullUrl, body) + ) { + return Promise.resolve(); + } else { + return fetch(flTransportFullUrl, { + method: 'POST', + body + }); + } } function addToQueue(evt: BatchEvent): void { @@ -159,11 +176,36 @@ export function transportHandler( } /** - * Force flush the queued events. Useful at page unload time to ensure all - * events are uploaded. + * Force flush the queued events. Useful at page unload time to ensure all events are uploaded. + * Flush will attempt to use sendBeacon to send events async and defaults back to fetch as soon as a + * sendBeacon fails. Firefox */ export function flushQueuedEvents(): void { + const flTransportFullUrl = + SettingsService.getInstance().getFlTransportFullUrl(); + while (queue.length > 0) { - dispatchQueueEvents(); + // Send the last events first to prioritize page load traces + const staged = queue.splice(-SettingsService.getInstance().logMaxFlushSize); + const body = buildPayload(staged); + + if ( + navigator.sendBeacon && + navigator.sendBeacon(flTransportFullUrl, body) + ) { + continue; + } else { + queue = [...queue, ...staged]; + break; + } + } + if (queue.length > 0) { + const body = buildPayload(queue); + fetch(flTransportFullUrl, { + method: 'POST', + body + }).catch(() => { + consoleLogger.info(`Failed flushing queued events.`); + }); } } diff --git a/packages/performance/src/utils/attributes_utils.ts b/packages/performance/src/utils/attributes_utils.ts index 81bb2234bfe..6306b4b9e48 100644 --- a/packages/performance/src/utils/attributes_utils.ts +++ b/packages/performance/src/utils/attributes_utils.ts @@ -70,7 +70,7 @@ interface NavigatorWithConnection extends Navigator { const RESERVED_ATTRIBUTE_PREFIXES = ['firebase_', 'google_', 'ga_']; const ATTRIBUTE_FORMAT_REGEX = new RegExp('^[a-zA-Z]\\w*$'); const MAX_ATTRIBUTE_NAME_LENGTH = 40; -const MAX_ATTRIBUTE_VALUE_LENGTH = 100; +export const MAX_ATTRIBUTE_VALUE_LENGTH = 100; export function getServiceWorkerStatus(): ServiceWorkerStatus { const navigator = Api.getInstance().navigator; diff --git a/packages/remote-config-compat/CHANGELOG.md b/packages/remote-config-compat/CHANGELOG.md index 052bc765c9c..08cd52e62d0 100644 --- a/packages/remote-config-compat/CHANGELOG.md +++ b/packages/remote-config-compat/CHANGELOG.md @@ -1,5 +1,34 @@ # @firebase/remote-config-compat +## 0.2.20 + +### Patch Changes + +- Updated dependencies [[`120a308`](https://github.com/firebase/firebase-js-sdk/commit/120a30838da50f5ade4f634e97c34cbfcaff41ba)]: + - @firebase/remote-config@0.7.0 + - @firebase/remote-config-types@0.5.0 + +## 0.2.19 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/remote-config@0.6.6 + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.2.18 + +### Patch Changes + +- Updated dependencies [[`13e6cce`](https://github.com/firebase/firebase-js-sdk/commit/13e6cce882d687e06c8d9bfb56895f8a77fc57b5), [`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/remote-config@0.6.5 + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 0.2.17 ### Patch Changes diff --git a/packages/remote-config-compat/package.json b/packages/remote-config-compat/package.json index 507bce652f3..b733ab07582 100644 --- a/packages/remote-config-compat/package.json +++ b/packages/remote-config-compat/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/remote-config-compat", - "version": "0.2.17", + "version": "0.2.20", "description": "The compatibility package of Remote Config", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -37,11 +37,11 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/remote-config": "0.6.4", - "@firebase/remote-config-types": "0.4.0", - "@firebase/util": "1.12.0", - "@firebase/logger": "0.4.4", - "@firebase/component": "0.6.17", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/logger": "0.5.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" }, "devDependencies": { @@ -50,7 +50,7 @@ "rollup-plugin-replace": "2.2.0", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4", - "@firebase/app-compat": "0.4.1" + "@firebase/app-compat": "0.5.3" }, "repository": { "directory": "packages/remote-config-compat", diff --git a/packages/remote-config-compat/rollup.config.js b/packages/remote-config-compat/rollup.config.js index e52495c401e..b96fe548483 100644 --- a/packages/remote-config-compat/rollup.config.js +++ b/packages/remote-config-compat/rollup.config.js @@ -30,7 +30,7 @@ const buildPlugins = [ typescript, tsconfigOverride: { compilerOptions: { - target: 'es2017' + target: 'es2020' } } }), diff --git a/packages/remote-config-types/CHANGELOG.md b/packages/remote-config-types/CHANGELOG.md index d4b52e801e8..ef86acb1e08 100644 --- a/packages/remote-config-types/CHANGELOG.md +++ b/packages/remote-config-types/CHANGELOG.md @@ -1,5 +1,11 @@ # @firebase/remote-config-types +## 0.5.0 + +### Minor Changes + +- [`120a308`](https://github.com/firebase/firebase-js-sdk/commit/120a30838da50f5ade4f634e97c34cbfcaff41ba) [#9221](https://github.com/firebase/firebase-js-sdk/pull/9221) - Added support for Realtime Remote Config for the web. This feature introduces a new `onConfigUpdate` API and allows web applications to receive near-instant configuration updates without requiring periodic polling. + ## 0.4.0 ### Minor Changes diff --git a/packages/remote-config-types/package.json b/packages/remote-config-types/package.json index 7de29a27043..ec02d236ec9 100644 --- a/packages/remote-config-types/package.json +++ b/packages/remote-config-types/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/remote-config-types", - "version": "0.4.0", + "version": "0.5.0", "description": "@firebase/remote-config Types", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", diff --git a/packages/remote-config/CHANGELOG.md b/packages/remote-config/CHANGELOG.md index 529e0417e75..fcf552b0e3e 100644 --- a/packages/remote-config/CHANGELOG.md +++ b/packages/remote-config/CHANGELOG.md @@ -1,5 +1,34 @@ # @firebase/remote-config +## 0.7.0 + +### Minor Changes + +- [`120a308`](https://github.com/firebase/firebase-js-sdk/commit/120a30838da50f5ade4f634e97c34cbfcaff41ba) [#9221](https://github.com/firebase/firebase-js-sdk/pull/9221) - Added support for Realtime Remote Config for the web. This feature introduces a new `onConfigUpdate` API and allows web applications to receive near-instant configuration updates without requiring periodic polling. + +## 0.6.6 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/installations@0.6.19 + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.6.5 + +### Patch Changes + +- [`13e6cce`](https://github.com/firebase/firebase-js-sdk/commit/13e6cce882d687e06c8d9bfb56895f8a77fc57b5) [#9085](https://github.com/firebase/firebase-js-sdk/pull/9085) - Add rollup config to generate modular typings for google3 + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/installations@0.6.18 + ## 0.6.4 ### Patch Changes diff --git a/packages/remote-config/package.json b/packages/remote-config/package.json index 43648d267d4..963b23acdcc 100644 --- a/packages/remote-config/package.json +++ b/packages/remote-config/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/remote-config", - "version": "0.6.4", + "version": "0.7.0", "description": "The Remote Config package of the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "browser": "dist/esm/index.esm2017.js", - "module": "dist/esm/index.esm2017.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/remote-config-public.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -40,15 +40,15 @@ "@firebase/app": "0.x" }, "dependencies": { - "@firebase/installations": "0.6.17", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "rollup-plugin-dts": "5.3.1", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/remote-config/rollup.config.js b/packages/remote-config/rollup.config.js index d8eb3abd315..8c7b834bf96 100644 --- a/packages/remote-config/rollup.config.js +++ b/packages/remote-config/rollup.config.js @@ -52,7 +52,7 @@ const esmBuild = { external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('esm', 2017)), + replace(generateBuildTargetReplaceConfig('esm', 2020)), emitModulePackageFile() ] }; @@ -67,7 +67,7 @@ const cjsBuild = { external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), plugins: [ ...buildPlugins, - replace(generateBuildTargetReplaceConfig('cjs', 2017)) + replace(generateBuildTargetReplaceConfig('cjs', 2020)) ] }; diff --git a/packages/remote-config/src/api.ts b/packages/remote-config/src/api.ts index 1431864edd5..62dc2697a64 100644 --- a/packages/remote-config/src/api.ts +++ b/packages/remote-config/src/api.ts @@ -22,7 +22,9 @@ import { LogLevel as RemoteConfigLogLevel, RemoteConfig, Value, - RemoteConfigOptions + RemoteConfigOptions, + ConfigUpdateObserver, + Unsubscribe } from './public_types'; import { RemoteConfigAbortSignal } from './client/remote_config_fetch_client'; import { @@ -66,6 +68,9 @@ export function getRemoteConfig( rc._initializePromise = Promise.all([ rc._storage.setLastSuccessfulFetchResponse(options.initialFetchResponse), rc._storage.setActiveConfigEtag(options.initialFetchResponse?.eTag || ''), + rc._storage.setActiveConfigTemplateVersion( + options.initialFetchResponse.templateVersion || 0 + ), rc._storageCache.setLastSuccessfulFetchTimestampMillis(Date.now()), rc._storageCache.setLastFetchStatus('success'), rc._storageCache.setActiveConfig( @@ -98,6 +103,7 @@ export async function activate(remoteConfig: RemoteConfig): Promise { !lastSuccessfulFetchResponse || !lastSuccessfulFetchResponse.config || !lastSuccessfulFetchResponse.eTag || + !lastSuccessfulFetchResponse.templateVersion || lastSuccessfulFetchResponse.eTag === activeConfigEtag ) { // Either there is no successful fetched config, or is the same as current active @@ -106,7 +112,10 @@ export async function activate(remoteConfig: RemoteConfig): Promise { } await Promise.all([ rc._storageCache.setActiveConfig(lastSuccessfulFetchResponse.config), - rc._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag) + rc._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag), + rc._storage.setActiveConfigTemplateVersion( + lastSuccessfulFetchResponse.templateVersion + ) ]); return true; } @@ -351,3 +360,30 @@ export async function setCustomSignals( ); } } + +// TODO: Add public document for the Remote Config Realtime API guide on the Web Platform. +/** + * Starts listening for real-time config updates from the Remote Config backend and automatically + * fetches updates from the Remote Config backend when they are available. + * + * @remarks + * If a connection to the Remote Config backend is not already open, calling this method will + * open it. Multiple listeners can be added by calling this method again, but subsequent calls + * re-use the same connection to the backend. + * + * @param remoteConfig - The {@link RemoteConfig} instance. + * @param observer - The {@link ConfigUpdateObserver} to be notified of config updates. + * @returns An {@link Unsubscribe} function to remove the listener. + * + * @public + */ +export function onConfigUpdate( + remoteConfig: RemoteConfig, + observer: ConfigUpdateObserver +): Unsubscribe { + const rc = getModularInstance(remoteConfig) as RemoteConfigImpl; + rc._realtimeHandler.addObserver(observer); + return () => { + rc._realtimeHandler.removeObserver(observer); + }; +} diff --git a/packages/remote-config/src/client/eventEmitter.ts b/packages/remote-config/src/client/eventEmitter.ts new file mode 100644 index 00000000000..10e2201ba2b --- /dev/null +++ b/packages/remote-config/src/client/eventEmitter.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from '@firebase/util'; + +// TODO: Consolidate the Visibility monitoring API code into a shared utility function in firebase/util to be used by both packages/database and packages/remote-config. +/** + * Base class to be used if you want to emit events. Call the constructor with + * the set of allowed event names. + */ +export abstract class EventEmitter { + private listeners_: { + [eventType: string]: Array<{ + callback(...args: unknown[]): void; + context: unknown; + }>; + } = {}; + + constructor(private allowedEvents_: string[]) { + assert( + Array.isArray(allowedEvents_) && allowedEvents_.length > 0, + 'Requires a non-empty array' + ); + } + + /** + * To be overridden by derived classes in order to fire an initial event when + * somebody subscribes for data. + * + * @returns {Array.<*>} Array of parameters to trigger initial event with. + */ + abstract getInitialEvent(eventType: string): unknown[]; + + /** + * To be called by derived classes to trigger events. + */ + protected trigger(eventType: string, ...varArgs: unknown[]): void { + if (Array.isArray(this.listeners_[eventType])) { + // Clone the list, since callbacks could add/remove listeners. + const listeners = [...this.listeners_[eventType]]; + + for (let i = 0; i < listeners.length; i++) { + listeners[i].callback.apply(listeners[i].context, varArgs); + } + } + } + + on( + eventType: string, + callback: (a: unknown) => void, + context: unknown + ): void { + this.validateEventType_(eventType); + this.listeners_[eventType] = this.listeners_[eventType] || []; + this.listeners_[eventType].push({ callback, context }); + + const eventData = this.getInitialEvent(eventType); + if (eventData) { + //@ts-ignore + callback.apply(context, eventData); + } + } + + off( + eventType: string, + callback: (a: unknown) => void, + context: unknown + ): void { + this.validateEventType_(eventType); + const listeners = this.listeners_[eventType] || []; + for (let i = 0; i < listeners.length; i++) { + if ( + listeners[i].callback === callback && + (!context || context === listeners[i].context) + ) { + listeners.splice(i, 1); + return; + } + } + } + + private validateEventType_(eventType: string): void { + assert( + this.allowedEvents_.find(et => { + return et === eventType; + }), + 'Unknown event: ' + eventType + ); + } +} diff --git a/packages/remote-config/src/client/realtime_handler.ts b/packages/remote-config/src/client/realtime_handler.ts new file mode 100644 index 00000000000..2ed244b5bd4 --- /dev/null +++ b/packages/remote-config/src/client/realtime_handler.ts @@ -0,0 +1,715 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { _FirebaseInstallationsInternal } from '@firebase/installations'; +import { Logger } from '@firebase/logger'; +import { + ConfigUpdate, + ConfigUpdateObserver, + FetchResponse, + FirebaseRemoteConfigObject +} from '../public_types'; +import { calculateBackoffMillis, FirebaseError } from '@firebase/util'; +import { ERROR_FACTORY, ErrorCode } from '../errors'; +import { Storage } from '../storage/storage'; +import { VisibilityMonitor } from './visibility_monitor'; +import { StorageCache } from '../storage/storage_cache'; +import { + FetchRequest, + RemoteConfigAbortSignal +} from './remote_config_fetch_client'; +import { CachingClient } from './caching_client'; + +const API_KEY_HEADER = 'X-Goog-Api-Key'; +const INSTALLATIONS_AUTH_TOKEN_HEADER = 'X-Goog-Firebase-Installations-Auth'; +const ORIGINAL_RETRIES = 8; +const MAXIMUM_FETCH_ATTEMPTS = 3; +const NO_BACKOFF_TIME_IN_MILLIS = -1; +const NO_FAILED_REALTIME_STREAMS = 0; +const REALTIME_DISABLED_KEY = 'featureDisabled'; +const REALTIME_RETRY_INTERVAL = 'retryIntervalSeconds'; +const TEMPLATE_VERSION_KEY = 'latestTemplateVersionNumber'; + +export class RealtimeHandler { + constructor( + private readonly firebaseInstallations: _FirebaseInstallationsInternal, + private readonly storage: Storage, + private readonly sdkVersion: string, + private readonly namespace: string, + private readonly projectId: string, + private readonly apiKey: string, + private readonly appId: string, + private readonly logger: Logger, + private readonly storageCache: StorageCache, + private readonly cachingClient: CachingClient + ) { + void this.setRetriesRemaining(); + void VisibilityMonitor.getInstance().on( + 'visible', + this.onVisibilityChange, + this + ); + } + + private observers: Set = + new Set(); + private isConnectionActive: boolean = false; + private isRealtimeDisabled: boolean = false; + private controller?: AbortController; + private reader: ReadableStreamDefaultReader | undefined; + private httpRetriesRemaining: number = ORIGINAL_RETRIES; + private isInBackground: boolean = false; + private readonly decoder = new TextDecoder('utf-8'); + private isClosingConnection: boolean = false; + + private async setRetriesRemaining(): Promise { + // Retrieve number of remaining retries from last session. The minimum retry count being one. + const metadata = await this.storage.getRealtimeBackoffMetadata(); + const numFailedStreams = metadata?.numFailedStreams || 0; + this.httpRetriesRemaining = Math.max( + ORIGINAL_RETRIES - numFailedStreams, + 1 + ); + } + + private propagateError = (e: FirebaseError): void => + this.observers.forEach(o => o.error?.(e)); + + /** + * Increment the number of failed stream attempts, increase the backoff duration, set the backoff + * end time to "backoff duration" after `lastFailedStreamTime` and persist the new + * values to storage metadata. + */ + private async updateBackoffMetadataWithLastFailedStreamConnectionTime( + lastFailedStreamTime: Date + ): Promise { + const numFailedStreams = + ((await this.storage.getRealtimeBackoffMetadata())?.numFailedStreams || + 0) + 1; + const backoffMillis = calculateBackoffMillis(numFailedStreams, 60000, 2); + await this.storage.setRealtimeBackoffMetadata({ + backoffEndTimeMillis: new Date( + lastFailedStreamTime.getTime() + backoffMillis + ), + numFailedStreams + }); + } + + /** + * Increase the backoff duration with a new end time based on Retry Interval. + */ + private async updateBackoffMetadataWithRetryInterval( + retryIntervalSeconds: number + ): Promise { + const currentTime = Date.now(); + const backoffDurationInMillis = retryIntervalSeconds * 1000; + const backoffEndTime = new Date(currentTime + backoffDurationInMillis); + const numFailedStreams = 0; + await this.storage.setRealtimeBackoffMetadata({ + backoffEndTimeMillis: backoffEndTime, + numFailedStreams + }); + await this.retryHttpConnectionWhenBackoffEnds(); + } + + /** + * HTTP status code that the Realtime client should retry on. + */ + private isStatusCodeRetryable = (statusCode?: number): boolean => { + const retryableStatusCodes = [ + 408, // Request Timeout + 429, // Too Many Requests + 502, // Bad Gateway + 503, // Service Unavailable + 504 // Gateway Timeout + ]; + return !statusCode || retryableStatusCodes.includes(statusCode); + }; + + /** + * Closes the realtime HTTP connection. + * Note: This method is designed to be called only once at a time. + * If a call is already in progress, subsequent calls will be ignored. + */ + private async closeRealtimeHttpConnection(): Promise { + if (this.isClosingConnection) { + return; + } + this.isClosingConnection = true; + + try { + if (this.reader) { + await this.reader.cancel(); + } + } catch (e) { + // The network connection was lost, so cancel() failed. + // This is expected in a disconnected state, so we can safely ignore the error. + this.logger.debug('Failed to cancel the reader, connection was lost.'); + } finally { + this.reader = undefined; + } + + if (this.controller) { + await this.controller.abort(); + this.controller = undefined; + } + + this.isClosingConnection = false; + } + + private async resetRealtimeBackoff(): Promise { + await this.storage.setRealtimeBackoffMetadata({ + backoffEndTimeMillis: new Date(-1), + numFailedStreams: 0 + }); + } + + private resetRetryCount(): void { + this.httpRetriesRemaining = ORIGINAL_RETRIES; + } + + /** + * Assembles the request headers and body and executes the fetch request to + * establish the real-time streaming connection. This is the "worker" method + * that performs the actual network communication. + */ + private async establishRealtimeConnection( + url: URL, + installationId: string, + installationTokenResult: string, + signal: AbortSignal + ): Promise { + const eTagValue = await this.storage.getActiveConfigEtag(); + const lastKnownVersionNumber = + await this.storage.getActiveConfigTemplateVersion(); + + const headers = { + [API_KEY_HEADER]: this.apiKey, + [INSTALLATIONS_AUTH_TOKEN_HEADER]: installationTokenResult, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'If-None-Match': eTagValue || '*', + 'Content-Encoding': 'gzip' + }; + + const requestBody = { + project: this.projectId, + namespace: this.namespace, + lastKnownVersionNumber, + appId: this.appId, + sdkVersion: this.sdkVersion, + appInstanceId: installationId + }; + + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(requestBody), + signal + }); + return response; + } + + private getRealtimeUrl(): URL { + const urlBase = + window.FIREBASE_REMOTE_CONFIG_URL_BASE || + 'https://firebaseremoteconfigrealtime.googleapis.com'; + + const urlString = `${urlBase}/v1/projects/${this.projectId}/namespaces/${this.namespace}:streamFetchInvalidations?key=${this.apiKey}`; + return new URL(urlString); + } + + private async createRealtimeConnection(): Promise { + const [installationId, installationTokenResult] = await Promise.all([ + this.firebaseInstallations.getId(), + this.firebaseInstallations.getToken(false) + ]); + this.controller = new AbortController(); + const url = this.getRealtimeUrl(); + const realtimeConnection = await this.establishRealtimeConnection( + url, + installationId, + installationTokenResult, + this.controller.signal + ); + return realtimeConnection; + } + + /** + * Retries HTTP stream connection asyncly in random time intervals. + */ + private async retryHttpConnectionWhenBackoffEnds(): Promise { + let backoffMetadata = await this.storage.getRealtimeBackoffMetadata(); + if (!backoffMetadata) { + backoffMetadata = { + backoffEndTimeMillis: new Date(NO_BACKOFF_TIME_IN_MILLIS), + numFailedStreams: NO_FAILED_REALTIME_STREAMS + }; + } + const backoffEndTime = new Date( + backoffMetadata.backoffEndTimeMillis + ).getTime(); + const currentTime = Date.now(); + const retryMillis = Math.max(0, backoffEndTime - currentTime); + await this.makeRealtimeHttpConnection(retryMillis); + } + + private setIsHttpConnectionRunning(connectionRunning: boolean): void { + this.isConnectionActive = connectionRunning; + } + + /** + * Combines the check and set operations to prevent multiple asynchronous + * calls from redundantly starting an HTTP connection. This ensures that + * only one attempt is made at a time. + */ + private checkAndSetHttpConnectionFlagIfNotRunning(): boolean { + const canMakeConnection = this.canEstablishStreamConnection(); + if (canMakeConnection) { + this.setIsHttpConnectionRunning(true); + } + return canMakeConnection; + } + + private fetchResponseIsUpToDate( + fetchResponse: FetchResponse, + lastKnownVersion: number + ): boolean { + // If there is a config, make sure its version is >= the last known version. + if (fetchResponse.config != null && fetchResponse.templateVersion) { + return fetchResponse.templateVersion >= lastKnownVersion; + } + // If there isn't a config, return true if the fetch was successful and backend had no update. + // Else, it returned an out of date config. + return this.storageCache.getLastFetchStatus() === 'success'; + } + + private parseAndValidateConfigUpdateMessage(message: string): string { + const left = message.indexOf('{'); + const right = message.indexOf('}', left); + + if (left < 0 || right < 0) { + return ''; + } + return left >= right ? '' : message.substring(left, right + 1); + } + + private isEventListenersEmpty(): boolean { + return this.observers.size === 0; + } + + private getRandomInt(max: number): number { + return Math.floor(Math.random() * max); + } + + private executeAllListenerCallbacks(configUpdate: ConfigUpdate): void { + this.observers.forEach(observer => observer.next(configUpdate)); + } + + /** + * Compares two configuration objects and returns a set of keys that have changed. + * A key is considered changed if it's new, removed, or has a different value. + */ + private getChangedParams( + newConfig: FirebaseRemoteConfigObject, + oldConfig: FirebaseRemoteConfigObject + ): Set { + const changedKeys = new Set(); + const newKeys = new Set(Object.keys(newConfig || {})); + const oldKeys = new Set(Object.keys(oldConfig || {})); + + for (const key of newKeys) { + if (!oldKeys.has(key) || newConfig[key] !== oldConfig[key]) { + changedKeys.add(key); + } + } + + for (const key of oldKeys) { + if (!newKeys.has(key)) { + changedKeys.add(key); + } + } + + return changedKeys; + } + + private async fetchLatestConfig( + remainingAttempts: number, + targetVersion: number + ): Promise { + const remainingAttemptsAfterFetch = remainingAttempts - 1; + const currentAttempt = MAXIMUM_FETCH_ATTEMPTS - remainingAttemptsAfterFetch; + const customSignals = this.storageCache.getCustomSignals(); + if (customSignals) { + this.logger.debug( + `Fetching config with custom signals: ${JSON.stringify(customSignals)}` + ); + } + const abortSignal = new RemoteConfigAbortSignal(); + try { + const fetchRequest: FetchRequest = { + cacheMaxAgeMillis: 0, + signal: abortSignal, + customSignals, + fetchType: 'REALTIME', + fetchAttempt: currentAttempt + }; + + const fetchResponse: FetchResponse = await this.cachingClient.fetch( + fetchRequest + ); + let activatedConfigs = await this.storage.getActiveConfig(); + + if (!this.fetchResponseIsUpToDate(fetchResponse, targetVersion)) { + this.logger.debug( + "Fetched template version is the same as SDK's current version." + + ' Retrying fetch.' + ); + // Continue fetching until template version number is greater than current. + await this.autoFetch(remainingAttemptsAfterFetch, targetVersion); + return; + } + + if (fetchResponse.config == null) { + this.logger.debug( + 'The fetch succeeded, but the backend had no updates.' + ); + return; + } + + if (activatedConfigs == null) { + activatedConfigs = {}; + } + + const updatedKeys = this.getChangedParams( + fetchResponse.config, + activatedConfigs + ); + + if (updatedKeys.size === 0) { + this.logger.debug('Config was fetched, but no params changed.'); + return; + } + + const configUpdate: ConfigUpdate = { + getUpdatedKeys(): Set { + return new Set(updatedKeys); + } + }; + this.executeAllListenerCallbacks(configUpdate); + } catch (e: unknown) { + const errorMessage = e instanceof Error ? e.message : String(e); + const error = ERROR_FACTORY.create(ErrorCode.CONFIG_UPDATE_NOT_FETCHED, { + originalErrorMessage: `Failed to auto-fetch config update: ${errorMessage}` + }); + this.propagateError(error); + } + } + + private async autoFetch( + remainingAttempts: number, + targetVersion: number + ): Promise { + if (remainingAttempts === 0) { + const error = ERROR_FACTORY.create(ErrorCode.CONFIG_UPDATE_NOT_FETCHED, { + originalErrorMessage: + 'Unable to fetch the latest version of the template.' + }); + this.propagateError(error); + return; + } + + const timeTillFetchSeconds = this.getRandomInt(4); + const timeTillFetchInMiliseconds = timeTillFetchSeconds * 1000; + + await new Promise(resolve => + setTimeout(resolve, timeTillFetchInMiliseconds) + ); + await this.fetchLatestConfig(remainingAttempts, targetVersion); + } + + /** + * Processes a stream of real-time messages for configuration updates. + * This method reassembles fragmented messages, validates and parses the JSON, + * and automatically fetches a new config if a newer template version is available. + * It also handles server-specified retry intervals and propagates errors for + * invalid messages or when real-time updates are disabled. + */ + private async handleNotifications( + reader: ReadableStreamDefaultReader + ): Promise { + let partialConfigUpdateMessage: string; + let currentConfigUpdateMessage = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + + partialConfigUpdateMessage = this.decoder.decode(value, { stream: true }); + currentConfigUpdateMessage += partialConfigUpdateMessage; + + if (partialConfigUpdateMessage.includes('}')) { + currentConfigUpdateMessage = this.parseAndValidateConfigUpdateMessage( + currentConfigUpdateMessage + ); + + if (currentConfigUpdateMessage.length === 0) { + continue; + } + + try { + const jsonObject = JSON.parse(currentConfigUpdateMessage); + + if (this.isEventListenersEmpty()) { + break; + } + + if ( + REALTIME_DISABLED_KEY in jsonObject && + jsonObject[REALTIME_DISABLED_KEY] === true + ) { + const error = ERROR_FACTORY.create( + ErrorCode.CONFIG_UPDATE_UNAVAILABLE, + { + originalErrorMessage: + 'The server is temporarily unavailable. Try again in a few minutes.' + } + ); + this.propagateError(error); + break; + } + + if (TEMPLATE_VERSION_KEY in jsonObject) { + const oldTemplateVersion = + await this.storage.getActiveConfigTemplateVersion(); + const targetTemplateVersion = Number( + jsonObject[TEMPLATE_VERSION_KEY] + ); + if ( + oldTemplateVersion && + targetTemplateVersion > oldTemplateVersion + ) { + await this.autoFetch( + MAXIMUM_FETCH_ATTEMPTS, + targetTemplateVersion + ); + } + } + + // This field in the response indicates that the realtime request should retry after the + // specified interval to establish a long-lived connection. This interval extends the + // backoff duration without affecting the number of retries, so it will not enter an + // exponential backoff state. + if (REALTIME_RETRY_INTERVAL in jsonObject) { + const retryIntervalSeconds = Number( + jsonObject[REALTIME_RETRY_INTERVAL] + ); + await this.updateBackoffMetadataWithRetryInterval( + retryIntervalSeconds + ); + } + } catch (e: unknown) { + this.logger.debug('Unable to parse latest config update message.', e); + const errorMessage = e instanceof Error ? e.message : String(e); + this.propagateError( + ERROR_FACTORY.create(ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID, { + originalErrorMessage: errorMessage + }) + ); + } + currentConfigUpdateMessage = ''; + } + } + } + + private async listenForNotifications( + reader: ReadableStreamDefaultReader + ): Promise { + try { + await this.handleNotifications(reader); + } catch (e) { + // If the real-time connection is at an unexpected lifecycle state when the app is + // backgrounded, it's expected closing the connection will throw an exception. + if (!this.isInBackground) { + // Otherwise, the real-time server connection was closed due to a transient issue. + this.logger.debug( + 'Real-time connection was closed due to an exception.' + ); + } + } + } + + /** + * Open the real-time connection, begin listening for updates, and auto-fetch when an update is + * received. + * + * If the connection is successful, this method will block on its thread while it reads the + * chunk-encoded HTTP body. When the connection closes, it attempts to reestablish the stream. + */ + private async prepareAndBeginRealtimeHttpStream(): Promise { + if (!this.checkAndSetHttpConnectionFlagIfNotRunning()) { + return; + } + + let backoffMetadata = await this.storage.getRealtimeBackoffMetadata(); + if (!backoffMetadata) { + backoffMetadata = { + backoffEndTimeMillis: new Date(NO_BACKOFF_TIME_IN_MILLIS), + numFailedStreams: NO_FAILED_REALTIME_STREAMS + }; + } + const backoffEndTime = backoffMetadata.backoffEndTimeMillis.getTime(); + if (Date.now() < backoffEndTime) { + await this.retryHttpConnectionWhenBackoffEnds(); + return; + } + + let response: Response | undefined; + let responseCode: number | undefined; + try { + response = await this.createRealtimeConnection(); + responseCode = response.status; + if (response.ok && response.body) { + this.resetRetryCount(); + await this.resetRealtimeBackoff(); + const reader = response.body.getReader(); + this.reader = reader; + // Start listening for realtime notifications. + await this.listenForNotifications(reader); + } + } catch (error) { + if (this.isInBackground) { + // It's possible the app was backgrounded while the connection was open, which + // threw an exception trying to read the response. No real error here, so treat + // this as a success, even if we haven't read a 200 response code yet. + this.resetRetryCount(); + } else { + //there might have been a transient error so the client will retry the connection. + this.logger.debug( + 'Exception connecting to real-time RC backend. Retrying the connection...:', + error + ); + } + } finally { + // Close HTTP connection and associated streams. + await this.closeRealtimeHttpConnection(); + this.setIsHttpConnectionRunning(false); + + // Update backoff metadata if the connection failed in the foreground. + const connectionFailed = + !this.isInBackground && + (responseCode === undefined || + this.isStatusCodeRetryable(responseCode)); + + if (connectionFailed) { + await this.updateBackoffMetadataWithLastFailedStreamConnectionTime( + new Date() + ); + } + // If responseCode is null then no connection was made to server and the SDK should still retry. + if (connectionFailed || response?.ok) { + await this.retryHttpConnectionWhenBackoffEnds(); + } else { + const errorMessage = `Unable to connect to the server. HTTP status code: ${responseCode}`; + const firebaseError = ERROR_FACTORY.create( + ErrorCode.CONFIG_UPDATE_STREAM_ERROR, + { + originalErrorMessage: errorMessage + } + ); + this.propagateError(firebaseError); + } + } + } + + /** + * Checks whether connection can be made or not based on some conditions + * @returns booelean + */ + private canEstablishStreamConnection(): boolean { + const hasActiveListeners = this.observers.size > 0; + const isNotDisabled = !this.isRealtimeDisabled; + const isNoConnectionActive = !this.isConnectionActive; + const inForeground = !this.isInBackground; + return ( + hasActiveListeners && + isNotDisabled && + isNoConnectionActive && + inForeground + ); + } + + private async makeRealtimeHttpConnection(delayMillis: number): Promise { + if (!this.canEstablishStreamConnection()) { + return; + } + if (this.httpRetriesRemaining > 0) { + this.httpRetriesRemaining--; + await new Promise(resolve => setTimeout(resolve, delayMillis)); + void this.prepareAndBeginRealtimeHttpStream(); + } else if (!this.isInBackground) { + const error = ERROR_FACTORY.create(ErrorCode.CONFIG_UPDATE_STREAM_ERROR, { + originalErrorMessage: + 'Unable to connect to the server. Check your connection and try again.' + }); + this.propagateError(error); + } + } + + private async beginRealtime(): Promise { + if (this.observers.size > 0) { + await this.makeRealtimeHttpConnection(0); + } + } + + /** + * Adds an observer to the realtime updates. + * @param observer The observer to add. + */ + addObserver(observer: ConfigUpdateObserver): void { + this.observers.add(observer); + void this.beginRealtime(); + } + + /** + * Removes an observer from the realtime updates. + * @param observer The observer to remove. + */ + removeObserver(observer: ConfigUpdateObserver): void { + if (this.observers.has(observer)) { + this.observers.delete(observer); + } + } + + /** + * Handles changes to the application's visibility state, managing the real-time connection. + * + * When the application is moved to the background, this method closes the existing + * real-time connection to save resources. When the application returns to the + * foreground, it attempts to re-establish the connection. + */ + private async onVisibilityChange(visible: unknown): Promise { + this.isInBackground = !visible; + if (!visible) { + await this.closeRealtimeHttpConnection(); + } else if (visible) { + await this.beginRealtime(); + } + } +} diff --git a/packages/remote-config/src/client/remote_config_fetch_client.ts b/packages/remote-config/src/client/remote_config_fetch_client.ts index 359bb7c0409..ddc1ba4279c 100644 --- a/packages/remote-config/src/client/remote_config_fetch_client.ts +++ b/packages/remote-config/src/client/remote_config_fetch_client.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { CustomSignals, FetchResponse } from '../public_types'; +import { CustomSignals, FetchResponse, FetchType } from '../public_types'; /** * Defines a client, as in https://en.wikipedia.org/wiki/Client%E2%80%93server_model, for the @@ -100,4 +100,18 @@ export interface FetchRequest { *

    Optional in case no custom signals are set for the instance. */ customSignals?: CustomSignals; + + /** + * The type of fetch to perform, such as a regular fetch or a real-time fetch. + * + * Optional as not all fetch requests need to be distinguished. + */ + fetchType?: FetchType; + + /** + * The number of fetch attempts made so far for this request. + * + * Optional as not all fetch requests are part of a retry series. + */ + fetchAttempt?: number; } diff --git a/packages/remote-config/src/client/rest_client.ts b/packages/remote-config/src/client/rest_client.ts index 57f55f53d88..42b0cab27c6 100644 --- a/packages/remote-config/src/client/rest_client.ts +++ b/packages/remote-config/src/client/rest_client.ts @@ -88,6 +88,8 @@ export class RestClient implements RemoteConfigFetchClient { // Deviates from pure decorator by not passing max-age header since we don't currently have // service behavior using that header. 'If-None-Match': request.eTag || '*' + // TODO: Add this header once CORS error is fixed internally. + //'X-Firebase-RC-Fetch-Type': `${fetchType}/${fetchAttempt}` }; const requestBody: FetchRequestBody = { @@ -140,6 +142,7 @@ export class RestClient implements RemoteConfigFetchClient { let config: FirebaseRemoteConfigObject | undefined; let state: string | undefined; + let templateVersion: number | undefined; // JSON parsing throws SyntaxError if the response body isn't a JSON string. // Requesting application/json and checking for a 200 ensures there's JSON data. @@ -154,6 +157,7 @@ export class RestClient implements RemoteConfigFetchClient { } config = responseBody['entries']; state = responseBody['state']; + templateVersion = responseBody['templateVersion']; } // Normalizes based on legacy state. @@ -176,6 +180,6 @@ export class RestClient implements RemoteConfigFetchClient { }); } - return { status, eTag: responseEtag, config }; + return { status, eTag: responseEtag, config, templateVersion }; } } diff --git a/packages/remote-config/src/client/visibility_monitor.ts b/packages/remote-config/src/client/visibility_monitor.ts new file mode 100644 index 00000000000..27028e3eeca --- /dev/null +++ b/packages/remote-config/src/client/visibility_monitor.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from '@firebase/util'; + +import { EventEmitter } from './eventEmitter'; + +declare const document: Document; + +// TODO: Consolidate the Visibility monitoring API code into a shared utility function in firebase/util to be used by both packages/database and packages/remote-config. +export class VisibilityMonitor extends EventEmitter { + private visible_: boolean; + + static getInstance(): VisibilityMonitor { + return new VisibilityMonitor(); + } + + constructor() { + super(['visible']); + let hidden: string; + let visibilityChange: string; + if ( + typeof document !== 'undefined' && + typeof document.addEventListener !== 'undefined' + ) { + if (typeof document['hidden'] !== 'undefined') { + // Opera 12.10 and Firefox 18 and later support + visibilityChange = 'visibilitychange'; + hidden = 'hidden'; + } // @ts-ignore + else if (typeof document['mozHidden'] !== 'undefined') { + visibilityChange = 'mozvisibilitychange'; + hidden = 'mozHidden'; + } // @ts-ignore + else if (typeof document['msHidden'] !== 'undefined') { + visibilityChange = 'msvisibilitychange'; + hidden = 'msHidden'; + } // @ts-ignore + else if (typeof document['webkitHidden'] !== 'undefined') { + visibilityChange = 'webkitvisibilitychange'; + hidden = 'webkitHidden'; + } + } + + // Initially, we always assume we are visible. This ensures that in browsers + // without page visibility support or in cases where we are never visible + // (e.g. chrome extension), we act as if we are visible, i.e. don't delay + // reconnects + this.visible_ = true; + + // @ts-ignore + if (visibilityChange) { + document.addEventListener( + visibilityChange, + () => { + // @ts-ignore + const visible = !document[hidden]; + if (visible !== this.visible_) { + this.visible_ = visible; + this.trigger('visible', visible); + } + }, + false + ); + } + } + + getInitialEvent(eventType: string): boolean[] { + assert(eventType === 'visible', 'Unknown event type: ' + eventType); + return [this.visible_]; + } +} diff --git a/packages/remote-config/src/errors.ts b/packages/remote-config/src/errors.ts index 446bd2c6e7a..dea9f43e922 100644 --- a/packages/remote-config/src/errors.ts +++ b/packages/remote-config/src/errors.ts @@ -33,7 +33,11 @@ export const enum ErrorCode { FETCH_PARSE = 'fetch-client-parse', FETCH_STATUS = 'fetch-status', INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable', - CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 'custom-signal-max-allowed-signals' + CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 'custom-signal-max-allowed-signals', + CONFIG_UPDATE_STREAM_ERROR = 'stream-error', + CONFIG_UPDATE_UNAVAILABLE = 'realtime-unavailable', + CONFIG_UPDATE_MESSAGE_INVALID = 'update-message-invalid', + CONFIG_UPDATE_NOT_FETCHED = 'update-not-fetched' } const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { @@ -72,7 +76,15 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { [ErrorCode.INDEXED_DB_UNAVAILABLE]: 'Indexed DB is not supported by current browser', [ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS]: - 'Setting more than {$maxSignals} custom signals is not supported.' + 'Setting more than {$maxSignals} custom signals is not supported.', + [ErrorCode.CONFIG_UPDATE_STREAM_ERROR]: + 'The stream was not able to connect to the backend: {$originalErrorMessage}.', + [ErrorCode.CONFIG_UPDATE_UNAVAILABLE]: + 'The Realtime service is unavailable: {$originalErrorMessage}', + [ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID]: + 'The stream invalidation message was unparsable: {$originalErrorMessage}', + [ErrorCode.CONFIG_UPDATE_NOT_FETCHED]: + 'Unable to fetch the latest config: {$originalErrorMessage}' }; // Note this is effectively a type system binding a code to params. This approach overlaps with the @@ -92,6 +104,10 @@ interface ErrorParams { [ErrorCode.FETCH_PARSE]: { originalErrorMessage: string }; [ErrorCode.FETCH_STATUS]: { httpStatus: number }; [ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS]: { maxSignals: number }; + [ErrorCode.CONFIG_UPDATE_STREAM_ERROR]: { originalErrorMessage: string }; + [ErrorCode.CONFIG_UPDATE_UNAVAILABLE]: { originalErrorMessage: string }; + [ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID]: { originalErrorMessage: string }; + [ErrorCode.CONFIG_UPDATE_NOT_FETCHED]: { originalErrorMessage: string }; } export const ERROR_FACTORY = new ErrorFactory( diff --git a/packages/remote-config/src/public_types.ts b/packages/remote-config/src/public_types.ts index 927bc84ca10..964726a51f4 100644 --- a/packages/remote-config/src/public_types.ts +++ b/packages/remote-config/src/public_types.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { FirebaseApp } from '@firebase/app'; +import { FirebaseApp, FirebaseError } from '@firebase/app'; /** * The Firebase Remote Config service interface. @@ -52,6 +52,8 @@ export interface RemoteConfig { /** * Defines a self-descriptive reference for config key-value pairs. + * + * @public */ export interface FirebaseRemoteConfigObject { [key: string]: string; @@ -62,6 +64,8 @@ export interface FirebaseRemoteConfigObject { * *

    Modeled after the native `Response` interface, but simplified for Remote Config's * use case. + * + * @public */ export interface FetchResponse { /** @@ -90,6 +94,11 @@ export interface FetchResponse { */ config?: FirebaseRemoteConfigObject; + /** + * The version number of the config template fetched from the server. + */ + templateVersion?: number; + // Note: we're not extracting experiment metadata until // ABT and Analytics have Web SDKs. } @@ -212,6 +221,63 @@ export interface CustomSignals { [key: string]: string | number | null; } +/** + * Contains information about which keys have been updated. + * + * @public + */ +export interface ConfigUpdate { + /** + * Parameter keys whose values have been updated from the currently activated values. + * Includes keys that are added, deleted, or whose value, value source, or metadata has changed. + */ + getUpdatedKeys(): Set; +} + +/** + * Observer interface for receiving real-time Remote Config update notifications. + * + * NOTE: Although an `complete` callback can be provided, it will + * never be called because the ConfigUpdate stream is never-ending. + * + * @public + */ +export interface ConfigUpdateObserver { + /** + * Called when a new ConfigUpdate is available. + */ + next: (configUpdate: ConfigUpdate) => void; + + /** + * Called if an error occurs during the stream. + */ + error: (error: FirebaseError) => void; + + /** + * Called when the stream is gracefully terminated. + */ + complete: () => void; +} + +/** + * A function that unsubscribes from a real-time event stream. + * + * @public + */ +export type Unsubscribe = () => void; + +/** + * Indicates the type of fetch request. + * + *

      + *
    • "BASE" indicates a standard fetch request.
    • + *
    • "REALTIME" indicates a fetch request triggered by a real-time update.
    • + *
    + * + * @public + */ +export type FetchType = 'BASE' | 'REALTIME'; + declare module '@firebase/component' { interface NameServiceMapping { 'remote-config': RemoteConfig; diff --git a/packages/remote-config/src/register.ts b/packages/remote-config/src/register.ts index dda6cc544de..eade371ca89 100644 --- a/packages/remote-config/src/register.ts +++ b/packages/remote-config/src/register.ts @@ -37,6 +37,7 @@ import { ErrorCode, ERROR_FACTORY } from './errors'; import { RemoteConfig as RemoteConfigImpl } from './remote_config'; import { IndexedDbStorage, InMemoryStorage } from './storage/storage'; import { StorageCache } from './storage/storage_cache'; +import { RealtimeHandler } from './client/realtime_handler'; // This needs to be in the same file that calls `getProvider()` on the component // or it will get tree-shaken out. import '@firebase/installations'; @@ -51,7 +52,7 @@ export function registerRemoteConfig(): void { ); registerVersion(packageName, version); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(packageName, version, '__BUILD_TARGET__'); function remoteConfigFactory( @@ -107,12 +108,26 @@ export function registerRemoteConfig(): void { logger ); + const realtimeHandler = new RealtimeHandler( + installations, + storage, + SDK_VERSION, + namespace, + projectId, + apiKey, + appId, + logger, + storageCache, + cachingClient + ); + const remoteConfigInstance = new RemoteConfigImpl( app, cachingClient, storageCache, storage, - logger + logger, + realtimeHandler ); // Starts warming cache. diff --git a/packages/remote-config/src/remote_config.ts b/packages/remote-config/src/remote_config.ts index bd2db66d0b3..bd32c938304 100644 --- a/packages/remote-config/src/remote_config.ts +++ b/packages/remote-config/src/remote_config.ts @@ -25,6 +25,7 @@ import { StorageCache } from './storage/storage_cache'; import { RemoteConfigFetchClient } from './client/remote_config_fetch_client'; import { Storage } from './storage/storage'; import { Logger } from '@firebase/logger'; +import { RealtimeHandler } from './client/realtime_handler'; const DEFAULT_FETCH_TIMEOUT_MILLIS = 60 * 1000; // One minute const DEFAULT_CACHE_MAX_AGE_MILLIS = 12 * 60 * 60 * 1000; // Twelve hours. @@ -83,6 +84,10 @@ export class RemoteConfig implements RemoteConfigType { /** * @internal */ - readonly _logger: Logger + readonly _logger: Logger, + /** + * @internal + */ + readonly _realtimeHandler: RealtimeHandler ) {} } diff --git a/packages/remote-config/src/storage/storage.ts b/packages/remote-config/src/storage/storage.ts index f03ff41377b..bd262d29968 100644 --- a/packages/remote-config/src/storage/storage.ts +++ b/packages/remote-config/src/storage/storage.ts @@ -56,6 +56,13 @@ export interface ThrottleMetadata { throttleEndTimeMillis: number; } +export interface RealtimeBackoffMetadata { + // The number of consecutive connection streams that have failed. + numFailedStreams: number; + // The Date until which the client should wait before attempting any new real-time connections. + backoffEndTimeMillis: Date; +} + /** * Provides type-safety for the "key" field used by {@link APP_NAMESPACE_STORE}. * @@ -69,7 +76,9 @@ type ProjectNamespaceKeyFieldValue = | 'last_successful_fetch_response' | 'settings' | 'throttle_metadata' - | 'custom_signals'; + | 'custom_signals' + | 'realtime_backoff_metadata' + | 'last_known_template_version'; // Visible for testing. export function openDatabase(): Promise { @@ -178,6 +187,27 @@ export abstract class Storage { abstract get(key: ProjectNamespaceKeyFieldValue): Promise; abstract set(key: ProjectNamespaceKeyFieldValue, value: T): Promise; abstract delete(key: ProjectNamespaceKeyFieldValue): Promise; + + getRealtimeBackoffMetadata(): Promise { + return this.get('realtime_backoff_metadata'); + } + + setRealtimeBackoffMetadata( + realtimeMetadata: RealtimeBackoffMetadata + ): Promise { + return this.set( + 'realtime_backoff_metadata', + realtimeMetadata + ); + } + + getActiveConfigTemplateVersion(): Promise { + return this.get('last_known_template_version'); + } + + setActiveConfigTemplateVersion(version: number): Promise { + return this.set('last_known_template_version', version); + } } export class IndexedDbStorage extends Storage { diff --git a/packages/remote-config/test/api.test.ts b/packages/remote-config/test/api.test.ts index b1fe658ebae..f38b4ca0bee 100644 --- a/packages/remote-config/test/api.test.ts +++ b/packages/remote-config/test/api.test.ts @@ -17,11 +17,13 @@ import { expect } from 'chai'; import { + ConfigUpdateObserver, ensureInitialized, fetchAndActivate, FetchResponse, getRemoteConfig, - getString + getString, + onConfigUpdate } from '../src'; import '../test/setup'; import { @@ -34,6 +36,8 @@ import * as sinon from 'sinon'; import { Component, ComponentType } from '@firebase/component'; import { FirebaseInstallations } from '@firebase/installations-types'; import { openDatabase, APP_NAMESPACE_STORE } from '../src/storage/storage'; +import { ERROR_FACTORY, ErrorCode } from '../src/errors'; +import { RemoteConfig as RemoteConfigImpl } from '../src/remote_config'; const fakeFirebaseConfig = { apiKey: 'api-key', @@ -45,6 +49,12 @@ const fakeFirebaseConfig = { appId: '1:111:web:a1234' }; +const mockObserver = { + next: sinon.stub(), + error: sinon.stub(), + complete: sinon.stub() +}; + async function clearDatabase(): Promise { const db = await openDatabase(); db.transaction([APP_NAMESPACE_STORE], 'readwrite') @@ -57,7 +67,8 @@ describe('Remote Config API', () => { const STUB_FETCH_RESPONSE: FetchResponse = { status: 200, eTag: 'asdf', - config: { 'foobar': 'hello world' } + config: { 'foobar': 'hello world' }, + templateVersion: 1 }; let fetchStub: sinon.SinonStub; @@ -94,7 +105,8 @@ describe('Remote Config API', () => { json: () => Promise.resolve({ entries: response.config, - state: 'OK' + state: 'OK', + templateVersion: response.templateVersion }) } as Response) ); @@ -149,4 +161,99 @@ describe('Remote Config API', () => { await ensureInitialized(rc); expect(getString(rc, 'foobar')).to.equal('hello world'); }); + + describe('onConfigUpdate', () => { + let capturedObserver: ConfigUpdateObserver | undefined; + let rc: RemoteConfigImpl; + let addObserverStub: sinon.SinonStub; + let removeObserverStub: sinon.SinonStub; + + beforeEach(() => { + rc = getRemoteConfig(app) as RemoteConfigImpl; + + addObserverStub = sinon + .stub(rc._realtimeHandler, 'addObserver') + .resolves(); + removeObserverStub = sinon + .stub(rc._realtimeHandler, 'removeObserver') + .resolves(); + + addObserverStub.callsFake(async (observer: ConfigUpdateObserver) => { + capturedObserver = observer; + }); + }); + + afterEach(() => { + capturedObserver = undefined; + addObserverStub.restore(); + removeObserverStub.restore(); + }); + + it('should call addObserver on the internal realtimeHandler', async () => { + await onConfigUpdate(rc, mockObserver); + expect(addObserverStub).to.have.been.calledOnce; + expect(addObserverStub).to.have.been.calledWith(mockObserver); + }); + + it('should return an unsubscribe function', async () => { + const unsubscribe = await onConfigUpdate(rc, mockObserver); + expect(unsubscribe).to.be.a('function'); + }); + + it('returned unsubscribe function should call removeObserver', async () => { + const unsubscribe = await onConfigUpdate(rc, mockObserver); + + unsubscribe(); + expect(removeObserverStub).to.have.been.calledOnce; + expect(removeObserverStub).to.have.been.calledWith(mockObserver); + }); + + it('observer.next should be called when realtimeHandler propagates an update', async () => { + await onConfigUpdate(rc, mockObserver); + + if (capturedObserver && capturedObserver.next) { + const mockConfigUpdate = { getUpdatedKeys: () => new Set(['new_key']) }; + capturedObserver.next(mockConfigUpdate); + } else { + expect.fail('Observer was not captured or next method is missing.'); + } + + expect(mockObserver.next).to.have.been.calledOnce; + expect(mockObserver.next).to.have.been.calledWithMatch({ + getUpdatedKeys: sinon.match.func + }); + expect( + mockObserver.next.getCall(0).args[0].getUpdatedKeys() + ).to.deep.equal(new Set(['new_key'])); + }); + + it('observer.error should be called when realtimeHandler propagates an error', async () => { + await onConfigUpdate(rc, mockObserver); + + if (capturedObserver && capturedObserver.error) { + const expectedOriginalErrorMessage = 'Realtime stream error'; + const mockError = ERROR_FACTORY.create( + ErrorCode.CONFIG_UPDATE_STREAM_ERROR, + { + originalErrorMessage: expectedOriginalErrorMessage + } + ); + capturedObserver.error(mockError); + } else { + expect.fail('Observer was not captured or error method is missing.'); + } + + expect(mockObserver.error).to.have.been.calledOnce; + const receivedError = mockObserver.error.getCall(0).args[0]; + + expect(receivedError.message).to.equal( + 'Remote Config: The stream was not able to connect to the backend: Realtime stream error. (remoteconfig/stream-error).' + ); + expect(receivedError).to.have.nested.property( + 'customData.originalErrorMessage', + 'Realtime stream error' + ); + expect((receivedError as any).code).to.equal('remoteconfig/stream-error'); + }); + }); }); diff --git a/packages/remote-config/test/client/realtime_handler.test.ts b/packages/remote-config/test/client/realtime_handler.test.ts new file mode 100644 index 00000000000..fbdbe982b8b --- /dev/null +++ b/packages/remote-config/test/client/realtime_handler.test.ts @@ -0,0 +1,911 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import * as sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { RealtimeHandler } from '../../src/client/realtime_handler'; +import { _FirebaseInstallationsInternal } from '@firebase/installations'; +import { Logger } from '@firebase/logger'; +import { Storage } from '../../src/storage/storage'; +import { StorageCache } from '../../src/storage/storage_cache'; +import { CachingClient } from '../../src/client/caching_client'; +import { ConfigUpdateObserver, FetchResponse } from '../../src/public_types'; +import { ErrorCode } from '../../src/errors'; +import { VisibilityMonitor } from '../../src/client/visibility_monitor'; + +use(sinonChai); + +const FAKE_APP_ID = '1:123456789:web:abcdef'; +const INSTALLATION_ID_STRING = 'installation-id-123'; +const INSTALLATION_AUTH_TOKEN_STRING = 'installation-auth-token-456'; +const PROJECT_NUMBER = '123456789'; +const API_KEY = 'api-key-123'; +const FAKE_NOW = 1234567890; +const ORIGINAL_RETRIES = 8; +const MAXIMUM_FETCH_ATTEMPTS = 3; + +const DUMMY_FETCH_RESPONSE: FetchResponse = { + status: 200, + config: { testKey: 'test_value' }, + eTag: 'etag-2', + templateVersion: 2 +}; + +// Helper to create a mock ReadableStream from a string array. +function createMockReadableStream( + chunks: string[] = [] +): ReadableStream { + const encoder = new TextEncoder(); + return new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + controller.enqueue(encoder.encode(chunk)); + } + controller.close(); + } + }); +} + +function createStreamingMockReader( + chunks: string[] +): ReadableStreamDefaultReader { + const stream = createMockReadableStream(chunks); + const reader = stream.getReader(); + const originalRead = reader.read; + sinon.stub(reader, 'read').callsFake(originalRead.bind(reader)); + return reader; +} + +describe('RealtimeHandler', () => { + let mockFetch: sinon.SinonStub; + let mockInstallations: sinon.SinonStubbedInstance<_FirebaseInstallationsInternal>; + let mockStorage: sinon.SinonStubbedInstance; + let mockStorageCache: sinon.SinonStubbedInstance; + let mockCachingClient: sinon.SinonStubbedInstance; + let mockLogger: sinon.SinonStubbedInstance; + let realtime: RealtimeHandler; + let clock: sinon.SinonFakeTimers; + let visibilityMonitorOnStub: sinon.SinonStub; + + beforeEach(async () => { + mockFetch = sinon.stub(window, 'fetch'); + mockInstallations = { + getId: sinon.stub().resolves(INSTALLATION_ID_STRING), + getToken: sinon.stub().resolves(INSTALLATION_AUTH_TOKEN_STRING) + } as any; + + mockLogger = sinon.createStubInstance(Logger); + + mockStorage = { + getRealtimeBackoffMetadata: sinon.stub().resolves(undefined), + setRealtimeBackoffMetadata: sinon.stub().resolves(), + getActiveConfigEtag: sinon.stub().resolves('etag-1'), + getActiveConfigTemplateVersion: sinon.stub().resolves(1), + getActiveConfig: sinon.stub().resolves({}), + + getLastFetchStatus: sinon.stub(), + setLastFetchStatus: sinon.stub(), + getLastSuccessfulFetchTimestampMillis: sinon.stub(), + setLastSuccessfulFetchTimestampMillis: sinon.stub(), + getLastSuccessfulFetchResponse: sinon.stub(), + setLastSuccessfulFetchResponse: sinon.stub(), + setActiveConfig: sinon.stub(), + setActiveConfigEtag: sinon.stub(), + getThrottleMetadata: sinon.stub(), + setThrottleMetadata: sinon.stub(), + deleteThrottleMetadata: sinon.stub(), + getCustomSignals: sinon.stub(), + setCustomSignals: sinon.stub(), + setActiveConfigTemplateVersion: sinon.stub() + } as sinon.SinonStubbedInstance; + + mockStorageCache = sinon.createStubInstance(StorageCache); + mockStorageCache.getLastFetchStatus.returns('success'); + mockStorageCache.getCustomSignals.returns(undefined); + + mockCachingClient = sinon.createStubInstance(CachingClient); + mockCachingClient.fetch.resolves(DUMMY_FETCH_RESPONSE); + + visibilityMonitorOnStub = sinon.stub(); + sinon.stub(VisibilityMonitor, 'getInstance').returns({ + on: visibilityMonitorOnStub + } as any); + + clock = sinon.useFakeTimers(FAKE_NOW); + + realtime = new RealtimeHandler( + mockInstallations, + mockStorage as any, + 'sdk-version', + 'namespace', + PROJECT_NUMBER, + API_KEY, + FAKE_APP_ID, + mockLogger as any, + mockStorageCache as any, + mockCachingClient as any + ); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + describe('constructor', () => { + it('should initialize with default retries if no backoff metadata in storage', async () => { + await clock.runAllAsync(); + expect((realtime as any).httpRetriesRemaining).to.equal(ORIGINAL_RETRIES); + }); + + it('should set retries remaining from storage if available', async () => { + mockStorage.getRealtimeBackoffMetadata.resolves({ + backoffEndTimeMillis: new Date(FAKE_NOW - 1000), // In the past, so no backoff + numFailedStreams: 3 + }); + + realtime = new RealtimeHandler( + mockInstallations, + mockStorage as any, + 'sdk-version', + 'namespace', + PROJECT_NUMBER, + API_KEY, + FAKE_APP_ID, + mockLogger as any, + mockStorageCache as any, + mockCachingClient as any + ); + await clock.runAllAsync(); + expect((realtime as any).httpRetriesRemaining).to.equal( + ORIGINAL_RETRIES - 3 + ); + }); + }); + + describe('getRealtimeUrl', () => { + it('should construct the correct URL', () => { + const url = (realtime as any).getRealtimeUrl(); + expect(url.toString()).to.equal( + `https://firebaseremoteconfigrealtime.googleapis.com/v1/projects/${PROJECT_NUMBER}/namespaces/namespace:streamFetchInvalidations?key=${API_KEY}` + ); + }); + + it('should use the URL base from window if it exists', () => { + (window as any).FIREBASE_REMOTE_CONFIG_URL_BASE = + 'https://test.googleapis.com'; + const url = (realtime as any).getRealtimeUrl(); + expect(url.toString()).to.equal( + `https://test.googleapis.com/v1/projects/${PROJECT_NUMBER}/namespaces/namespace:streamFetchInvalidations?key=${API_KEY}` + ); + delete (window as any).FIREBASE_REMOTE_CONFIG_URL_BASE; + }); + }); + + describe('isStatusCodeRetryable', () => { + it('should return true for retryable status codes', () => { + const retryableCodes = [408, 429, 502, 503, 504]; + retryableCodes.forEach(code => { + expect((realtime as any).isStatusCodeRetryable(code)).to.be.true; + }); + }); + + it('should return true for undefined status code', () => { + expect((realtime as any).isStatusCodeRetryable(undefined)).to.be.true; + }); + + it('should return false for non-retryable status codes', () => { + // This is a sample of non-retryable codes for testing purposes. + const nonRetryableCodes = [200, 304, 400, 401, 403]; + nonRetryableCodes.forEach(code => { + expect((realtime as any).isStatusCodeRetryable(code)).to.be.false; + }); + }); + }); + + describe('updateBackoffMetadataWithLastFailedStreamConnectionTime', () => { + it('should increment numFailedStreams and set backoffEndTimeMillis', async () => { + const spy = mockStorage.setRealtimeBackoffMetadata; + const lastFailedTime = new Date(FAKE_NOW); + + await ( + realtime as any + ).updateBackoffMetadataWithLastFailedStreamConnectionTime(lastFailedTime); + + expect(spy).to.have.been.calledOnce; + const metadata = spy.getCall(0).args[0]; + expect(metadata.numFailedStreams).to.equal(1); + expect(metadata.backoffEndTimeMillis.getTime()).to.be.greaterThan( + lastFailedTime.getTime() + ); + }); + }); + + describe('updateBackoffMetadataWithRetryInterval', () => { + it('should set backoffEndTimeMillis based on provided retryIntervalSeconds and then retry connection', async () => { + const setMetadataSpy = mockStorage.setRealtimeBackoffMetadata; + const retryHttpConnectionSpy = sinon.spy( + realtime as any, + 'retryHttpConnectionWhenBackoffEnds' + ); + const retryInterval = 10; + + await (realtime as any).updateBackoffMetadataWithRetryInterval( + retryInterval + ); + + expect(setMetadataSpy).to.have.been.calledOnce; + const metadata = setMetadataSpy.getCall(0).args[0]; + expect(metadata.backoffEndTimeMillis.getTime()).to.be.closeTo( + FAKE_NOW + retryInterval * 1000, + 100 + ); + expect(retryHttpConnectionSpy).to.have.been.calledOnce; + }); + }); + + describe('closeRealtimeHttpConnection', () => { + let mockController: sinon.SinonStubbedInstance; + let mockReader: sinon.SinonStubbedInstance< + ReadableStreamDefaultReader + >; + + beforeEach(() => { + mockController = sinon.createStubInstance(AbortController); + mockReader = sinon.createStubInstance(ReadableStreamDefaultReader); + (realtime as any).controller = mockController; + (realtime as any).reader = mockReader; + }); + + it('should abort controller and cancel reader', async () => { + await (realtime as any).closeRealtimeHttpConnection(); + expect(mockController.abort).to.have.been.calledOnce; + expect(mockReader.cancel).to.have.been.calledOnce; + expect((realtime as any).controller).to.be.undefined; + expect((realtime as any).reader).to.be.undefined; + }); + + it('should handle reader cancellation failure gracefully', async () => { + mockReader.cancel.rejects(new Error('test error')); + await (realtime as any).closeRealtimeHttpConnection(); + expect(mockLogger.debug).to.have.been.calledWith( + 'Failed to cancel the reader, connection was lost.' + ); + // Should still clear reader + expect((realtime as any).reader).to.be.undefined; + }); + + it('should handle being called when reader is already undefined', async () => { + (realtime as any).reader = undefined; + await (realtime as any).closeRealtimeHttpConnection(); + expect(mockController.abort).to.have.been.calledOnce; + expect((realtime as any).controller).to.be.undefined; + }); + + it('should handle being called when controller is already undefined', async () => { + (realtime as any).controller = undefined; + await (realtime as any).closeRealtimeHttpConnection(); + expect(mockReader.cancel).to.have.been.calledOnce; + expect((realtime as any).reader).to.be.undefined; + }); + }); + + describe('resetRealtimeBackoff', () => { + it('should reset backoff metadata in storage', async () => { + const spy = mockStorage.setRealtimeBackoffMetadata; + await (realtime as any).resetRealtimeBackoff(); + expect(spy).to.have.been.calledOnce; + const metadata = spy.getCall(0).args[0]; + expect(metadata.numFailedStreams).to.equal(0); + expect(metadata.backoffEndTimeMillis.getTime()).to.equal(-1); + }); + }); + + describe('establishRealtimeConnection', () => { + it('should send correct headers and body for realtime connection', async () => { + mockStorage.getActiveConfigEtag.resolves('current-etag'); + mockStorage.getActiveConfigTemplateVersion.resolves(10); + + const url = new URL('https://example.com/stream'); + const signal = new AbortController().signal; + + await (realtime as any).establishRealtimeConnection( + url, + INSTALLATION_ID_STRING, + INSTALLATION_AUTH_TOKEN_STRING, + signal + ); + + expect(mockFetch).to.have.been.calledOnce; + const [fetchUrl, fetchOptions] = mockFetch.getCall(0).args; + expect(fetchUrl).to.equal(url); + expect(fetchOptions.method).to.equal('POST'); + expect(fetchOptions.headers).to.deep.include({ + 'X-Goog-Api-Key': API_KEY, + 'X-Goog-Firebase-Installations-Auth': INSTALLATION_AUTH_TOKEN_STRING, + 'Content-Type': 'application/json', + Accept: 'application/json', + 'If-None-Match': 'current-etag', + 'Content-Encoding': 'gzip' + }); + const body = JSON.parse(fetchOptions.body as string); + expect(body).to.deep.equal({ + project: PROJECT_NUMBER, + namespace: 'namespace', + lastKnownVersionNumber: 10, + appId: FAKE_APP_ID, + sdkVersion: 'sdk-version', + appInstanceId: INSTALLATION_ID_STRING + }); + }); + }); + + describe('retryHttpConnectionWhenBackoffEnds', () => { + let makeRealtimeHttpConnectionSpy: sinon.SinonSpy; + + beforeEach(() => { + makeRealtimeHttpConnectionSpy = sinon.spy( + realtime as any, + 'makeRealtimeHttpConnection' + ); + }); + + it('should call makeRealtimeHttpConnection with 0 delay if no backoff metadata', async () => { + mockStorage.getRealtimeBackoffMetadata.resolves(undefined); + await (realtime as any).retryHttpConnectionWhenBackoffEnds(); + expect(makeRealtimeHttpConnectionSpy).to.have.been.calledWith(0); + }); + + it('should call makeRealtimeHttpConnection with calculated delay if backoff metadata exists', async () => { + mockStorage.getRealtimeBackoffMetadata.resolves({ + // 5 seconds in the future + backoffEndTimeMillis: new Date(FAKE_NOW + 5000), + numFailedStreams: 1 + }); + await (realtime as any).retryHttpConnectionWhenBackoffEnds(); + expect(makeRealtimeHttpConnectionSpy).to.have.been.calledOnce; + const delay = makeRealtimeHttpConnectionSpy.getCall(0).args[0]; + expect(delay).to.be.closeTo(5000, 100); + }); + }); + + describe('fetchResponseIsUpToDate', () => { + it('should return true if templateVersion is greater or equal', () => { + const fetchResponse: FetchResponse = { + config: { k: 'v' }, + templateVersion: 5, + status: 200, + eTag: 'e' + }; + const result = (realtime as any).fetchResponseIsUpToDate( + fetchResponse, + 5 + ); + expect(result).to.be.true; + }); + + it('should return false if templateVersion is smaller', () => { + const fetchResponse: FetchResponse = { + config: { k: 'v' }, + templateVersion: 4, + status: 200, + eTag: 'e' + }; + const result = (realtime as any).fetchResponseIsUpToDate( + fetchResponse, + 5 + ); + expect(result).to.be.false; + }); + + it('should return true if no config and lastFetchStatus is success', () => { + const fetchResponse: FetchResponse = { + config: undefined, + templateVersion: undefined, + status: 304, + eTag: 'e' + }; + mockStorageCache.getLastFetchStatus.returns('success'); + const result = (realtime as any).fetchResponseIsUpToDate( + fetchResponse, + 5 + ); + expect(result).to.be.true; + }); + + it('should return false if no config and lastFetchStatus is not success', () => { + const fetchResponse: FetchResponse = { + config: undefined, + templateVersion: undefined, + status: 304, + eTag: 'e' + }; + mockStorageCache.getLastFetchStatus.returns('throttle'); // Or any other non-'success' status + const result = (realtime as any).fetchResponseIsUpToDate( + fetchResponse, + 5 + ); + expect(result).to.be.false; + }); + }); + + describe('fetchLatestConfig', () => { + let autoFetchSpy: sinon.SinonSpy; + let executeAllListenerCallbacksSpy: sinon.SinonSpy; + + beforeEach(() => { + autoFetchSpy = sinon.spy(realtime as any, 'autoFetch'); + executeAllListenerCallbacksSpy = sinon.spy( + realtime as any, + 'executeAllListenerCallbacks' + ); + mockStorage.getActiveConfig.resolves({ existingKey: 'value' }); + mockStorage.getActiveConfigTemplateVersion.resolves(1); + }); + + afterEach(() => { + autoFetchSpy.restore(); + executeAllListenerCallbacksSpy.restore(); + }); + + it('should fetch, identify changed keys, and notify observers', async () => { + mockCachingClient.fetch.resolves({ + config: { existingKey: 'new_value', newKey: 'value' }, + templateVersion: 2, + status: 200, + eTag: 'e' + }); + + await (realtime as any).fetchLatestConfig(MAXIMUM_FETCH_ATTEMPTS, 2); + + expect(mockCachingClient.fetch).to.have.been.calledOnce; + expect(executeAllListenerCallbacksSpy).to.have.been.calledOnce; + const configUpdate = executeAllListenerCallbacksSpy.getCall(0).args[0]; + expect(configUpdate.getUpdatedKeys()).to.deep.equal( + new Set(['existingKey', 'newKey']) + ); + }); + + it('should retry with autoFetch if fetched version is not up-to-date', async () => { + autoFetchSpy.restore(); + const autoFetchStub = sinon.stub(realtime as any, 'autoFetch'); + + mockCachingClient.fetch.resolves({ + config: { k: 'v' }, + templateVersion: 1, + status: 200, + eTag: 'e' + }); + mockStorage.getActiveConfigTemplateVersion.resolves(0); + + await (realtime as any).fetchLatestConfig(MAXIMUM_FETCH_ATTEMPTS, 2); + + expect(mockCachingClient.fetch).to.have.been.calledOnce; + expect(autoFetchStub).to.have.been.calledOnceWith( + MAXIMUM_FETCH_ATTEMPTS - 1, + 2 + ); + }); + + it('should not notify if no keys have changed', async () => { + mockCachingClient.fetch.resolves({ + config: { existingKey: 'value' }, + templateVersion: 2, + status: 200, + eTag: 'e' + }); + + await (realtime as any).fetchLatestConfig(MAXIMUM_FETCH_ATTEMPTS, 2); + + expect(executeAllListenerCallbacksSpy).not.to.have.been.called; + }); + + it('should propagate error on fetch failure', async () => { + const testError = new Error('Network failed'); + mockCachingClient.fetch.rejects(testError); + const propagateErrorSpy = sinon.spy(realtime as any, 'propagateError'); + + await (realtime as any).fetchLatestConfig(MAXIMUM_FETCH_ATTEMPTS, 2); + + expect(propagateErrorSpy).to.have.been.calledOnce; + const error = propagateErrorSpy.getCall(0).args[0]; + expect(error.code).to.include(ErrorCode.CONFIG_UPDATE_NOT_FETCHED); + }); + + it('should include custom signals in fetch request', async () => { + mockStorageCache.getCustomSignals.returns({ signal1: 'value1' }); + + await (realtime as any).fetchLatestConfig(MAXIMUM_FETCH_ATTEMPTS, 2); + expect(mockLogger.debug).to.have.been.calledWith( + `Fetching config with custom signals: {"signal1":"value1"}` + ); + }); + + it('should handle null activatedConfigs gracefully', async () => { + mockCachingClient.fetch.resolves({ + config: { newKey: 'value' }, + templateVersion: 2, + status: 200, + eTag: 'e' + }); + mockStorage.getActiveConfig.resolves(null as any); + + await (realtime as any).fetchLatestConfig(MAXIMUM_FETCH_ATTEMPTS, 2); + + expect(executeAllListenerCallbacksSpy).to.have.been.calledOnce; + const configUpdate = executeAllListenerCallbacksSpy.getCall(0).args[0]; + expect(configUpdate.getUpdatedKeys()).to.deep.equal(new Set(['newKey'])); + }); + }); + + describe('autoFetch', () => { + let fetchLatestConfigStub: sinon.SinonStub; + let propagateErrorSpy: sinon.SinonSpy; + + beforeEach(() => { + fetchLatestConfigStub = sinon.stub(realtime as any, 'fetchLatestConfig'); + propagateErrorSpy = sinon.spy(realtime as any, 'propagateError'); + }); + + afterEach(() => { + fetchLatestConfigStub.restore(); + propagateErrorSpy.restore(); + }); + + it('should call fetchLatestConfig after a random delay', async () => { + (realtime as any).autoFetch(MAXIMUM_FETCH_ATTEMPTS, 10); + await clock.runAllAsync(); + + expect(fetchLatestConfigStub).to.have.been.calledOnceWith( + MAXIMUM_FETCH_ATTEMPTS, + 10 + ); + }); + + it('should propagate an error if remaining attempts is zero', async () => { + await (realtime as any).autoFetch(0, 10); + expect(propagateErrorSpy).to.have.been.calledOnce; + const error = propagateErrorSpy.getCall(0).args[0]; + expect(error.code).to.include(ErrorCode.CONFIG_UPDATE_NOT_FETCHED); + expect(fetchLatestConfigStub).not.to.have.been.called; + }); + }); + + describe('handleNotifications', () => { + let mockReader: ReadableStreamDefaultReader; + let autoFetchSpy: sinon.SinonSpy; + let executeAllListenerCallbacksSpy: sinon.SinonSpy; + let propagateErrorSpy: sinon.SinonSpy; + + beforeEach(() => { + autoFetchSpy = sinon.spy(realtime as any, 'autoFetch'); + executeAllListenerCallbacksSpy = sinon.spy( + realtime as any, + 'executeAllListenerCallbacks' + ); + propagateErrorSpy = sinon.spy(realtime as any, 'propagateError'); + (realtime as any).observers.add({}); + }); + + afterEach(() => { + autoFetchSpy.restore(); + executeAllListenerCallbacksSpy.restore(); + propagateErrorSpy.restore(); + }); + + it('should set backoff metadata if REALTIME_RETRY_INTERVAL is present', async () => { + const updateBackoffStub = sinon + .stub(realtime as any, 'updateBackoffMetadataWithRetryInterval') + .resolves(); + + mockReader = createStreamingMockReader(['{"retryIntervalSeconds": 60}']); + + await (realtime as any).handleNotifications(mockReader); + + expect(updateBackoffStub).to.have.been.calledOnceWith(60); + }); + + it('should propagate error on invalid JSON', async () => { + mockReader = createStreamingMockReader(['{invalid_json}']); + + await (realtime as any).handleNotifications(mockReader); + + expect(propagateErrorSpy).to.have.been.calledOnce; + const error = propagateErrorSpy.getCall(0).args[0]; + expect(error.code).to.include(ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID); + }); + + it('should break if event listeners become empty during handling', async () => { + autoFetchSpy.restore(); + + mockReader = createStreamingMockReader([ + '{"latestTemplateVersionNumber": 10}' + ]); + mockStorage.getActiveConfigTemplateVersion.resolves(5); + mockCachingClient.fetch.resolves({ + config: { k: 'v' }, + templateVersion: 10, + status: 200, + eTag: 'e' + }); + + const observer = (realtime as any).observers.values().next().value; + const originalJsonParse = JSON.parse; + JSON.parse = (text: string) => { + (realtime as any).observers.delete(observer); + return originalJsonParse(text); + }; + + await (realtime as any).handleNotifications(mockReader); + + expect(mockReader.read).to.have.been.calledOnce; + + JSON.parse = originalJsonParse; + }); + }); + + describe('beginRealtimeHttpStream', () => { + let createRealtimeConnectionSpy: sinon.SinonStub; + let listenForNotificationsSpy: sinon.SinonSpy; + let closeRealtimeHttpConnectionSpy: sinon.SinonSpy; + let retryHttpConnectionWhenBackoffEndsSpy: sinon.SinonStub; + let updateBackoffMetadataWithLastFailedStreamConnectionTimeSpy: sinon.SinonSpy; + let propagateErrorSpy: sinon.SinonSpy; + let checkAndSetHttpConnectionFlagIfNotRunningSpy: sinon.SinonStub; + + beforeEach(() => { + createRealtimeConnectionSpy = sinon.stub( + realtime as any, + 'createRealtimeConnection' + ); + listenForNotificationsSpy = sinon.spy( + realtime as any, + 'listenForNotifications' + ); + closeRealtimeHttpConnectionSpy = sinon.spy( + realtime as any, + 'closeRealtimeHttpConnection' + ); + + retryHttpConnectionWhenBackoffEndsSpy = sinon + .stub(realtime as any, 'retryHttpConnectionWhenBackoffEnds') + .resolves(); + updateBackoffMetadataWithLastFailedStreamConnectionTimeSpy = sinon.spy( + realtime as any, + 'updateBackoffMetadataWithLastFailedStreamConnectionTime' + ); + propagateErrorSpy = sinon.spy(realtime as any, 'propagateError'); + checkAndSetHttpConnectionFlagIfNotRunningSpy = sinon + .stub(realtime as any, 'checkAndSetHttpConnectionFlagIfNotRunning') + .returns(true); + + createRealtimeConnectionSpy.resolves( + new Response(createMockReadableStream(), { status: 200 }) + ); + + mockStorage.getRealtimeBackoffMetadata.resolves({ + backoffEndTimeMillis: new Date(-1), + numFailedStreams: 0 + }); + (realtime as any).httpRetriesRemaining = ORIGINAL_RETRIES; + }); + + afterEach(() => { + retryHttpConnectionWhenBackoffEndsSpy.restore(); + }); + + it('should successfully establish and handle a connection', async () => { + const resetRealtimeBackoffSpy = sinon.spy( + realtime as any, + 'resetRealtimeBackoff' + ); + (realtime as any).observers.add({}); + await (realtime as any).prepareAndBeginRealtimeHttpStream(); + + expect(createRealtimeConnectionSpy).to.have.been.calledOnce; + expect(listenForNotificationsSpy).to.have.been.calledOnce; + expect(resetRealtimeBackoffSpy).to.have.been.calledOnce; + expect(closeRealtimeHttpConnectionSpy).to.have.been.calledOnce; + expect(retryHttpConnectionWhenBackoffEndsSpy).to.have.been.calledOnce; + }); + + it('should return early if connection flag cannot be set', async () => { + checkAndSetHttpConnectionFlagIfNotRunningSpy.returns(false); + await (realtime as any).prepareAndBeginRealtimeHttpStream(); + expect(createRealtimeConnectionSpy).not.to.have.been.called; + }); + + it('should retry if currently in backoff period', async () => { + mockStorage.getRealtimeBackoffMetadata.resolves({ + backoffEndTimeMillis: new Date(FAKE_NOW + 1000), + numFailedStreams: 1 + }); + await (realtime as any).prepareAndBeginRealtimeHttpStream(); + expect(retryHttpConnectionWhenBackoffEndsSpy).to.have.been.calledOnce; + expect(createRealtimeConnectionSpy).not.to.have.been.called; + }); + + it('should update backoff metadata on connection failure in foreground', async () => { + (realtime as any).httpRetriesRemaining = 1; + + createRealtimeConnectionSpy.resolves(new Response(null, { status: 502 })); + (realtime as any).observers.add({}); + + await (realtime as any).prepareAndBeginRealtimeHttpStream(); + + expect(updateBackoffMetadataWithLastFailedStreamConnectionTimeSpy).to.have + .been.calledOnce; + expect(retryHttpConnectionWhenBackoffEndsSpy).to.have.been.calledOnce; + }); + + it('should NOT schedule a retry on connection failure in background', async () => { + (realtime as any).isInBackground = true; + + (realtime as any).observers.add({}); + + createRealtimeConnectionSpy.resolves(new Response(null, { status: 503 })); + + await (realtime as any).prepareAndBeginRealtimeHttpStream(); + + expect(updateBackoffMetadataWithLastFailedStreamConnectionTimeSpy).not.to + .have.been.called; + + expect(retryHttpConnectionWhenBackoffEndsSpy).not.to.have.been.called; + }); + + it('should propagate CONFIG_UPDATE_STREAM_ERROR if connection fails non-retryably', async () => { + (realtime as any).httpRetriesRemaining = 1; + createRealtimeConnectionSpy.resolves(new Response(null, { status: 400 })); + (realtime as any).observers.add({}); + + await (realtime as any).prepareAndBeginRealtimeHttpStream(); + + expect(retryHttpConnectionWhenBackoffEndsSpy).not.to.have.been.called; + expect(propagateErrorSpy).to.have.been.calledOnce; + }); + + it('should not propagate error if connection fails non-retryably in background', async () => { + (realtime as any).httpRetriesRemaining = 1; + createRealtimeConnectionSpy.resolves(new Response(null, { status: 400 })); + (realtime as any).observers.add({}); + (realtime as any).isInBackground = true; + + await (realtime as any).prepareAndBeginRealtimeHttpStream(); + + expect(propagateErrorSpy).to.have.been.calledOnce; + }); + + it('should propagate CONFIG_UPDATE_STREAM_ERROR if retries are exhausted', async () => { + (realtime as any).httpRetriesRemaining = 0; + (realtime as any).observers.add({}); + await (realtime as any).makeRealtimeHttpConnection(0); + + expect(propagateErrorSpy).to.have.been.calledOnce; + const error = propagateErrorSpy.getCall(0).args[0]; + expect(error.code).to.include(ErrorCode.CONFIG_UPDATE_STREAM_ERROR); + }); + + it('should handle rejection from createRealtimeConnection', async () => { + const testError = new Error('Connection refused'); + createRealtimeConnectionSpy.rejects(testError); + (realtime as any).observers.add({}); + + await (realtime as any).prepareAndBeginRealtimeHttpStream(); + + expect(updateBackoffMetadataWithLastFailedStreamConnectionTimeSpy).to.have + .been.calledOnce; + expect(retryHttpConnectionWhenBackoffEndsSpy).to.have.been.calledOnce; + }); + }); + + describe('canEstablishStreamConnection', () => { + it('returns true if all conditions are met', () => { + (realtime as any).observers.add({}); + (realtime as any).isRealtimeDisabled = false; + (realtime as any).isConnectionActive = false; + (realtime as any).isInBackground = false; + expect((realtime as any).canEstablishStreamConnection()).to.be.true; + }); + + it('returns false if there are no observers', () => { + (realtime as any).observers.clear(); + expect((realtime as any).canEstablishStreamConnection()).to.be.false; + }); + + it('returns false if realtime is disabled', () => { + (realtime as any).observers.add({}); + (realtime as any).isRealtimeDisabled = true; + expect((realtime as any).canEstablishStreamConnection()).to.be.false; + }); + + it('returns false if a connection is already active', () => { + (realtime as any).observers.add({}); + (realtime as any).isConnectionActive = true; + expect((realtime as any).canEstablishStreamConnection()).to.be.false; + }); + + it('returns false if app is in background', () => { + (realtime as any).observers.add({}); + (realtime as any).isInBackground = true; + expect((realtime as any).canEstablishStreamConnection()).to.be.false; + }); + }); + + describe('addObserver/removeObserver', () => { + let beginRealtimeStub: sinon.SinonStub; + const observer: ConfigUpdateObserver = { + next: () => {}, + error: () => {}, + complete: () => {} + }; + + beforeEach(() => { + beginRealtimeStub = sinon + .stub(realtime as any, 'beginRealtime') + .resolves(); + }); + + afterEach(() => { + beginRealtimeStub.restore(); + }); + + it('addObserver should add an observer and start the realtime connection', async () => { + await realtime.addObserver(observer); + expect((realtime as any).observers.has(observer)).to.be.true; + + expect(beginRealtimeStub).to.have.been.calledOnce; + }); + + it('removeObserver should remove an observer', () => { + (realtime as any).observers.add(observer); + realtime.removeObserver(observer); + expect((realtime as any).observers.has(observer)).to.be.false; + }); + }); + describe('onVisibilityChange', () => { + let closeConnectionSpy: sinon.SinonSpy; + let beginRealtimeSpy: sinon.SinonSpy; + + beforeEach(() => { + closeConnectionSpy = sinon.spy( + realtime as any, + 'closeRealtimeHttpConnection' + ); + beginRealtimeSpy = sinon.spy(realtime as any, 'beginRealtime'); + }); + + afterEach(() => { + closeConnectionSpy.restore(); + beginRealtimeSpy.restore(); + }); + + it('should close connection when app goes to background', async () => { + await (realtime as any).onVisibilityChange(false); + expect((realtime as any).isInBackground).to.be.true; + expect(closeConnectionSpy).to.have.been.calledOnce; + expect(beginRealtimeSpy).not.to.have.been.called; + }); + + it('should start connection when app comes to foreground', async () => { + await (realtime as any).onVisibilityChange(true); + expect((realtime as any).isInBackground).to.be.false; + expect(closeConnectionSpy).not.to.have.been.called; + expect(beginRealtimeSpy).to.have.been.calledOnce; + }); + }); +}); diff --git a/packages/remote-config/test/client/rest_client.test.ts b/packages/remote-config/test/client/rest_client.test.ts index 96a6cde8454..bda6fbce01a 100644 --- a/packages/remote-config/test/client/rest_client.test.ts +++ b/packages/remote-config/test/client/rest_client.test.ts @@ -26,6 +26,7 @@ import { FetchRequest, RemoteConfigAbortSignal } from '../../src/client/remote_config_fetch_client'; +import { Storage } from '../../src/storage/storage'; const DEFAULT_REQUEST: FetchRequest = { cacheMaxAgeMillis: 1, @@ -34,6 +35,7 @@ const DEFAULT_REQUEST: FetchRequest = { describe('RestClient', () => { const firebaseInstallations = {} as FirebaseInstallations; + const storage = {} as Storage; let client: RestClient; beforeEach(() => { @@ -51,6 +53,7 @@ describe('RestClient', () => { firebaseInstallations.getToken = sinon .stub() .returns(Promise.resolve('fis-token')); + storage.setActiveConfigTemplateVersion = sinon.stub(); }); describe('fetch', () => { @@ -74,7 +77,8 @@ describe('RestClient', () => { status: 200, eTag: 'etag', state: 'UPDATE', - entries: { color: 'sparkling' } + entries: { color: 'sparkling' }, + templateVersion: 1 }; fetchStub.returns( @@ -85,7 +89,8 @@ describe('RestClient', () => { json: () => Promise.resolve({ entries: expectedResponse.entries, - state: expectedResponse.state + state: expectedResponse.state, + templateVersion: expectedResponse.templateVersion }) } as Response) ); @@ -95,7 +100,8 @@ describe('RestClient', () => { expect(response).to.deep.eq({ status: expectedResponse.status, eTag: expectedResponse.eTag, - config: expectedResponse.entries + config: expectedResponse.entries, + templateVersion: expectedResponse.templateVersion }); }); @@ -184,7 +190,8 @@ describe('RestClient', () => { expect(response).to.deep.eq({ status: 304, eTag: 'response-etag', - config: undefined + config: undefined, + templateVersion: undefined }); }); @@ -222,7 +229,8 @@ describe('RestClient', () => { expect(response).to.deep.eq({ status: 304, eTag: 'etag', - config: undefined + config: undefined, + templateVersion: undefined }); }); @@ -239,7 +247,8 @@ describe('RestClient', () => { await expect(client.fetch(DEFAULT_REQUEST)).to.eventually.be.deep.eq({ status: 200, eTag: 'etag', - config: {} + config: {}, + templateVersion: undefined }); } }); diff --git a/packages/remote-config/test/remote_config.test.ts b/packages/remote-config/test/remote_config.test.ts index 8010f54f26d..1cc6b62717e 100644 --- a/packages/remote-config/test/remote_config.test.ts +++ b/packages/remote-config/test/remote_config.test.ts @@ -46,6 +46,7 @@ import { import * as api from '../src/api'; import { fetchAndActivate } from '../src'; import { restore } from 'sinon'; +import { RealtimeHandler } from '../src/client/realtime_handler'; describe('RemoteConfig', () => { const ACTIVE_CONFIG = { @@ -67,6 +68,7 @@ describe('RemoteConfig', () => { let storageCache: StorageCache; let storage: Storage; let logger: Logger; + let realtimeHandler: RealtimeHandler; let rc: RemoteConfigType; let getActiveConfigStub: sinon.SinonStub; @@ -79,12 +81,20 @@ describe('RemoteConfig', () => { client = {} as RemoteConfigFetchClient; storageCache = {} as StorageCache; storage = {} as Storage; + realtimeHandler = {} as RealtimeHandler; logger = new Logger('package-name'); getActiveConfigStub = sinon.stub().returns(undefined); storageCache.getActiveConfig = getActiveConfigStub; loggerDebugSpy = sinon.spy(logger, 'debug'); loggerLogLevelSpy = sinon.spy(logger, 'logLevel', ['set']); - rc = new RemoteConfig(app, client, storageCache, storage, logger); + rc = new RemoteConfig( + app, + client, + storageCache, + storage, + logger, + realtimeHandler + ); }); afterEach(() => { @@ -380,39 +390,56 @@ describe('RemoteConfig', () => { const ETAG = 'etag'; const CONFIG = { key: 'val' }; const NEW_ETAG = 'new_etag'; + const TEMPLATE_VERSION = 1; let getLastSuccessfulFetchResponseStub: sinon.SinonStub; let getActiveConfigEtagStub: sinon.SinonStub; + let getActiveConfigTemplateVersionStub: sinon.SinonStub; let setActiveConfigEtagStub: sinon.SinonStub; let setActiveConfigStub: sinon.SinonStub; + let setActiveConfigTemplateVersionStub: sinon.SinonStub; beforeEach(() => { getLastSuccessfulFetchResponseStub = sinon.stub(); getActiveConfigEtagStub = sinon.stub(); + getActiveConfigTemplateVersionStub = sinon.stub(); setActiveConfigEtagStub = sinon.stub(); setActiveConfigStub = sinon.stub(); + setActiveConfigTemplateVersionStub = sinon.stub(); storage.getLastSuccessfulFetchResponse = getLastSuccessfulFetchResponseStub; storage.getActiveConfigEtag = getActiveConfigEtagStub; + storage.getActiveConfigTemplateVersion = + getActiveConfigTemplateVersionStub; storage.setActiveConfigEtag = setActiveConfigEtagStub; storageCache.setActiveConfig = setActiveConfigStub; + storage.setActiveConfigTemplateVersion = + setActiveConfigTemplateVersionStub; }); it('does not activate if last successful fetch response is undefined', async () => { getLastSuccessfulFetchResponseStub.returns(Promise.resolve()); getActiveConfigEtagStub.returns(Promise.resolve(ETAG)); + getActiveConfigTemplateVersionStub.returns( + Promise.resolve(TEMPLATE_VERSION) + ); const activateResponse = await activate(rc); expect(activateResponse).to.be.false; expect(storage.setActiveConfigEtag).to.not.have.been.called; expect(storageCache.setActiveConfig).to.not.have.been.called; + expect(storage.setActiveConfigTemplateVersion).to.not.have.been.called; }); it('does not activate if fetched and active etags are the same', async () => { getLastSuccessfulFetchResponseStub.returns( - Promise.resolve({ config: {}, etag: ETAG }) + Promise.resolve({ + config: {}, + eTag: ETAG, + templateVersion: TEMPLATE_VERSION + }) ); getActiveConfigEtagStub.returns(Promise.resolve(ETAG)); @@ -421,11 +448,16 @@ describe('RemoteConfig', () => { expect(activateResponse).to.be.false; expect(storage.setActiveConfigEtag).to.not.have.been.called; expect(storageCache.setActiveConfig).to.not.have.been.called; + expect(storage.setActiveConfigTemplateVersion).to.not.have.been.called; }); it('activates if fetched and active etags are different', async () => { getLastSuccessfulFetchResponseStub.returns( - Promise.resolve({ config: CONFIG, eTag: NEW_ETAG }) + Promise.resolve({ + config: CONFIG, + eTag: NEW_ETAG, + templateVersion: TEMPLATE_VERSION + }) ); getActiveConfigEtagStub.returns(Promise.resolve(ETAG)); @@ -434,11 +466,18 @@ describe('RemoteConfig', () => { expect(activateResponse).to.be.true; expect(storage.setActiveConfigEtag).to.have.been.calledWith(NEW_ETAG); expect(storageCache.setActiveConfig).to.have.been.calledWith(CONFIG); + expect(storage.setActiveConfigTemplateVersion).to.have.been.calledWith( + TEMPLATE_VERSION + ); }); it('activates if fetched is defined but active config is not', async () => { getLastSuccessfulFetchResponseStub.returns( - Promise.resolve({ config: CONFIG, eTag: NEW_ETAG }) + Promise.resolve({ + config: CONFIG, + eTag: NEW_ETAG, + templateVersion: TEMPLATE_VERSION + }) ); getActiveConfigEtagStub.returns(Promise.resolve()); @@ -447,6 +486,9 @@ describe('RemoteConfig', () => { expect(activateResponse).to.be.true; expect(storage.setActiveConfigEtag).to.have.been.calledWith(NEW_ETAG); expect(storageCache.setActiveConfig).to.have.been.calledWith(CONFIG); + expect(storage.setActiveConfigTemplateVersion).to.have.been.calledWith( + TEMPLATE_VERSION + ); }); }); diff --git a/packages/rules-unit-testing/CHANGELOG.md b/packages/rules-unit-testing/CHANGELOG.md index dbdb276a1d8..6eaa82e889a 100644 --- a/packages/rules-unit-testing/CHANGELOG.md +++ b/packages/rules-unit-testing/CHANGELOG.md @@ -1,5 +1,16 @@ # @firebase/rules-unit-testing +## 5.0.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- Updated dependencies [[`a4ccd25`](https://github.com/firebase/firebase-js-sdk/commit/a4ccd254dd1ecb63aa010ca010ad50d4b8a8316a), [`5200f7b`](https://github.com/firebase/firebase-js-sdk/commit/5200f7bb777cf2260dcd396fbd19ac6cc7cb44c4), [`6ab4e13`](https://github.com/firebase/firebase-js-sdk/commit/6ab4e13a1665dab4be89ecc141b4584a5a6df569), [`91fa484`](https://github.com/firebase/firebase-js-sdk/commit/91fa484b5a6081ad9c59d3b62416a2b5252b95a6), [`e59cd7d`](https://github.com/firebase/firebase-js-sdk/commit/e59cd7da1f375ec89f237ceb684c9f450d65cd34), [`cb19688`](https://github.com/firebase/firebase-js-sdk/commit/cb19688bf3d339a46c4964cb30b6263af08526e6), [`d91169f`](https://github.com/firebase/firebase-js-sdk/commit/d91169f061bf1dcbfe78a8c8a7f739677608fcb7), [`ec5f374`](https://github.com/firebase/firebase-js-sdk/commit/ec5f37403d9ebe28d3d71a7789d59edfb12762df), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - firebase@12.0.0 + ## 4.0.1 ### Patch Changes diff --git a/packages/rules-unit-testing/package.json b/packages/rules-unit-testing/package.json index 033e4f752bc..68657c9cac8 100644 --- a/packages/rules-unit-testing/package.json +++ b/packages/rules-unit-testing/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/rules-unit-testing", - "version": "4.0.1", + "version": "5.0.0", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -20,7 +20,7 @@ "./package.json": "./package.json" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "dist" @@ -43,7 +43,7 @@ "rollup-plugin-typescript2": "0.36.0" }, "peerDependencies": { - "firebase": "^11.0.0" + "firebase": "^12.0.0" }, "repository": { "directory": "packages/rules-unit-testing", diff --git a/packages/storage-compat/CHANGELOG.md b/packages/storage-compat/CHANGELOG.md index 7988847b87b..ec3d3308432 100644 --- a/packages/storage-compat/CHANGELOG.md +++ b/packages/storage-compat/CHANGELOG.md @@ -1,5 +1,29 @@ # @firebase/storage-compat +## 0.4.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/storage@0.14.0 + - @firebase/util@1.13.0 + +## 0.3.24 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/storage@0.13.14 + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 0.3.23 ### Patch Changes diff --git a/packages/storage-compat/package.json b/packages/storage-compat/package.json index c5a31d6c6f0..cf86443967c 100644 --- a/packages/storage-compat/package.json +++ b/packages/storage-compat/package.json @@ -1,16 +1,16 @@ { "name": "@firebase/storage-compat", - "version": "0.3.23", + "version": "0.4.0", "description": "The Firebase Firestore compatibility package", "author": "Firebase (https://firebase.google.com/)", "main": "./dist/index.cjs.js", - "browser": "./dist/esm/index.esm2017.js", - "module": "./dist/esm/index.esm2017.js", + "browser": "./dist/esm/index.esm.js", + "module": "./dist/esm/index.esm.js", "exports": { ".": { "types": "./dist/src/index.d.ts", "require": "./dist/index.cjs.js", - "default": "./dist/esm/index.esm2017.js" + "default": "./dist/esm/index.esm.js" }, "./package.json": "./package.json" }, @@ -37,15 +37,15 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/storage": "0.13.13", + "@firebase/storage": "0.14.0", "@firebase/storage-types": "0.8.3", - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.4.1", - "@firebase/auth-compat": "0.5.27", + "@firebase/app-compat": "0.5.3", + "@firebase/auth-compat": "0.6.0", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", @@ -63,6 +63,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/storage-compat/rollup.config.js b/packages/storage-compat/rollup.config.js index 59f6282bf5e..5e0753ffdb8 100644 --- a/packages/storage-compat/rollup.config.js +++ b/packages/storage-compat/rollup.config.js @@ -31,7 +31,7 @@ const buildPlugins = [ abortOnError: false, tsconfigOverride: { compilerOptions: { - target: 'es2017' + target: 'es2020' } } }), diff --git a/packages/storage/CHANGELOG.md b/packages/storage/CHANGELOG.md index af1ec2bd2f0..a8d7d80f3fd 100644 --- a/packages/storage/CHANGELOG.md +++ b/packages/storage/CHANGELOG.md @@ -1,5 +1,29 @@ #Unreleased +## 0.14.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/util@1.13.0 + +## 0.13.14 + +### Patch Changes + +- [`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83) [#9111](https://github.com/firebase/firebase-js-sdk/pull/9111) - Fixed issue where Storage on Firebase Studio throws CORS errors. + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + ## 0.13.13 ### Patch Changes diff --git a/packages/storage/package.json b/packages/storage/package.json index bb4a5004840..6248ead4f5c 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,11 +1,11 @@ { "name": "@firebase/storage", - "version": "0.13.13", + "version": "0.14.0", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", - "module": "dist/index.esm2017.js", - "browser": "dist/index.esm2017.js", + "module": "dist/index.esm.js", + "browser": "dist/index.esm.js", "exports": { ".": { "types": "./dist/storage-public.d.ts", @@ -15,9 +15,9 @@ }, "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/index.esm2017.js" + "import": "./dist/index.esm.js" }, - "default": "./dist/index.esm2017.js" + "default": "./dist/index.esm.js" }, "./package.json": "./package.json" }, @@ -46,16 +46,16 @@ }, "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.12.0", - "@firebase/component": "0.6.17", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" }, "devDependencies": { - "@firebase/app": "0.13.1", - "@firebase/auth": "1.10.7", + "@firebase/app": "0.14.3", + "@firebase/auth": "1.11.0", "rollup": "2.79.2", "@rollup/plugin-alias": "5.1.1", "@rollup/plugin-json": "6.1.0", @@ -72,6 +72,6 @@ }, "typings": "dist/src/index.d.ts", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/storage/rollup.config.js b/packages/storage/rollup.config.js index 4ce92722281..c16231aa476 100644 --- a/packages/storage/rollup.config.js +++ b/packages/storage/rollup.config.js @@ -65,7 +65,7 @@ const browserBuilds = [ alias(generateAliasConfig('browser')), ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('esm', 2017), + ...generateBuildTargetReplaceConfig('esm', 2020), '__RUNTIME_ENV__': '' }) ], @@ -85,7 +85,7 @@ const browserBuilds = [ alias(generateAliasConfig('browser')), ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('cjs', 2017), + ...generateBuildTargetReplaceConfig('cjs', 2020), '__RUNTIME_ENV__': '' }) ], @@ -104,7 +104,7 @@ const browserBuilds = [ alias(generateAliasConfig('browser')), ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('cjs', 2017), + ...generateBuildTargetReplaceConfig('cjs', 2020), '__RUNTIME_ENV__': '' }) ], @@ -127,7 +127,7 @@ const nodeBuilds = [ alias(generateAliasConfig('node')), ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('cjs', 2017), + ...generateBuildTargetReplaceConfig('cjs', 2020), '__RUNTIME_ENV__': 'node' }) ], @@ -148,7 +148,7 @@ const nodeBuilds = [ alias(generateAliasConfig('node')), ...buildPlugins, replace({ - ...generateBuildTargetReplaceConfig('esm', 2017), + ...generateBuildTargetReplaceConfig('esm', 2020), '__RUNTIME_ENV__': 'node' }), emitModulePackageFile() diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index 733a39c9ad8..c23acc0ec4c 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -70,7 +70,7 @@ function registerStorage(): void { ); //RUNTIME_ENV will be replaced during the compilation to "node" for nodejs and an empty string for browser registerVersion(name, version, '__RUNTIME_ENV__'); - // BUILD_TARGET will be replaced by values like esm2017, cjs2017, etc during the compilation + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation registerVersion(name, version, '__BUILD_TARGET__'); } diff --git a/packages/template/CHANGELOG.md b/packages/template/CHANGELOG.md index 564baf12f32..d0c317e841e 100644 --- a/packages/template/CHANGELOG.md +++ b/packages/template/CHANGELOG.md @@ -1,5 +1,11 @@ # @firebase/template +## 0.2.7 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + ## 0.2.6 ### Patch Changes diff --git a/packages/template/package.json b/packages/template/package.json index 9274862e9d1..6ef9c2d81be 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -1,12 +1,12 @@ { "name": "@firebase/template", - "version": "0.2.6", + "version": "0.2.7", "private": true, "description": "A template package for new firebase packages", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm2017.js", - "module": "dist/index.esm2017.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", "exports": { ".": { "types": "./dist/index.d.ts", @@ -16,9 +16,9 @@ }, "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/index.esm2017.js" + "import": "./dist/index.esm.js" }, - "default": "./dist/index.esm2017.js" + "default": "./dist/index.esm.js" }, "./package.json": "./package.json" }, @@ -48,7 +48,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.13.1", + "@firebase/app": "0.14.3", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" diff --git a/packages/util/CHANGELOG.md b/packages/util/CHANGELOG.md index c208cce58db..38660ea5f50 100644 --- a/packages/util/CHANGELOG.md +++ b/packages/util/CHANGELOG.md @@ -1,5 +1,21 @@ # @firebase/util +## 1.13.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +## 1.12.1 + +### Patch Changes + +- [`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83) [#9111](https://github.com/firebase/firebase-js-sdk/pull/9111) - Fixed issue where Storage on Firebase Studio throws CORS errors. + ## 1.12.0 ### Minor Changes diff --git a/packages/util/package.json b/packages/util/package.json index 8a1ebb49dd5..8c44efc4924 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,11 +1,11 @@ { "name": "@firebase/util", - "version": "1.12.0", + "version": "1.13.0", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm2017.js", - "module": "dist/index.esm2017.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", "exports": { ".": { "types": "./dist/util-public.d.ts", @@ -15,9 +15,9 @@ }, "browser": { "require": "./dist/index.cjs.js", - "import": "./dist/index.esm2017.js" + "import": "./dist/index.esm.js" }, - "default": "./dist/index.esm2017.js" + "default": "./dist/index.esm.js" }, "./package.json": "./package.json" }, @@ -68,6 +68,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/packages/webchannel-wrapper/CHANGELOG.md b/packages/webchannel-wrapper/CHANGELOG.md index 789b6d26f7e..e415fdd90bc 100644 --- a/packages/webchannel-wrapper/CHANGELOG.md +++ b/packages/webchannel-wrapper/CHANGELOG.md @@ -1,5 +1,17 @@ # @firebase/webchannel-wrapper +## 1.0.5 + +### Patch Changes + +- [`43276b0`](https://github.com/firebase/firebase-js-sdk/commit/43276b0414ea5a73e8d8f7e3b80275d8b910102f) [#9242](https://github.com/firebase/firebase-js-sdk/pull/9242) - Increased the buffering-proxy detection timeout to minimize the false-positive rate. Updating WebChannel to ignore duplicate messages received from the server. Fix for https://github.com/firebase/firebase-js-sdk/issues/8250. + +## 1.0.4 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + ## 1.0.3 ### Patch Changes diff --git a/packages/webchannel-wrapper/package.json b/packages/webchannel-wrapper/package.json index 07b6385ed07..bddf3cec8ed 100644 --- a/packages/webchannel-wrapper/package.json +++ b/packages/webchannel-wrapper/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/webchannel-wrapper", - "version": "1.0.3", + "version": "1.0.5", "description": "A wrapper of the webchannel packages from closure-library for use outside of a closure compiled application", "author": "Firebase (https://firebase.google.com/)", "main": "empty.js", @@ -31,7 +31,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "closure-net": "git+https://github.com/google/closure-net.git#0412666", + "closure-net": "git+https://github.com/google/closure-net.git#6f48f578d3e80fe7a85e530a5d95b9351433d135", "@rollup/plugin-commonjs": "21.1.0", "rollup": "2.79.2", "rollup-plugin-copy": "3.5.0", diff --git a/packages/webchannel-wrapper/rollup.config.js b/packages/webchannel-wrapper/rollup.config.js index ae80c831396..18d7604133a 100644 --- a/packages/webchannel-wrapper/rollup.config.js +++ b/packages/webchannel-wrapper/rollup.config.js @@ -39,7 +39,7 @@ const buildPlugins = [ typescript, tsconfigOverride: { compilerOptions: { - target: 'es2017' + target: 'es2020' } } }), @@ -49,7 +49,7 @@ const buildPlugins = [ /** * ESM builds */ -const esm2017Builds = [ +const esmBuilds = [ { input: join(closureBlobsDir, 'webchannel_blob_es2018.js'), output: { @@ -70,4 +70,4 @@ const esm2017Builds = [ } ]; -export default esm2017Builds; +export default esmBuilds; diff --git a/repo-scripts/api-documenter/package.json b/repo-scripts/api-documenter/package.json index 585890922a1..6305cbf9372 100644 --- a/repo-scripts/api-documenter/package.json +++ b/repo-scripts/api-documenter/package.json @@ -37,6 +37,6 @@ "mocha-chai-jest-snapshot": "1.1.6" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/repo-scripts/changelog-generator/package.json b/repo-scripts/changelog-generator/package.json index 961620142a0..8b3acaade05 100644 --- a/repo-scripts/changelog-generator/package.json +++ b/repo-scripts/changelog-generator/package.json @@ -40,6 +40,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/repo-scripts/changelog-generator/tsconfig.json b/repo-scripts/changelog-generator/tsconfig.json index 38bdb7035e4..4d15389487e 100644 --- a/repo-scripts/changelog-generator/tsconfig.json +++ b/repo-scripts/changelog-generator/tsconfig.json @@ -9,6 +9,6 @@ "moduleResolution": "node", "esModuleInterop": true, "resolveJsonModule": true, - "target": "es2017" + "target": "es2020" } } \ No newline at end of file diff --git a/repo-scripts/prune-dts/package.json b/repo-scripts/prune-dts/package.json index 7a0890b00e2..e94519863a2 100644 --- a/repo-scripts/prune-dts/package.json +++ b/repo-scripts/prune-dts/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "description": "A script to prune non-exported types from a d.ts.", "author": "Firebase (https://firebase.google.com/)", diff --git a/repo-scripts/prune-dts/tsconfig.json b/repo-scripts/prune-dts/tsconfig.json index 14618a434b0..a4cb1ed68a0 100644 --- a/repo-scripts/prune-dts/tsconfig.json +++ b/repo-scripts/prune-dts/tsconfig.json @@ -5,7 +5,7 @@ "module": "commonjs", "moduleResolution": "node", "resolveJsonModule": true, - "target": "es2017", + "target": "es2020", "esModuleInterop": true, "declaration": true, "strict": true, diff --git a/repo-scripts/size-analysis/analysis-helper.ts b/repo-scripts/size-analysis/analysis-helper.ts index 9507bfe253c..a21215c777d 100644 --- a/repo-scripts/size-analysis/analysis-helper.ts +++ b/repo-scripts/size-analysis/analysis-helper.ts @@ -86,7 +86,7 @@ export async function extractDependenciesAndSize( input, plugins: [ resolve({ - mainFields: ['esm2017', 'module', 'main'] + mainFields: ['module', 'main'] }), commonjs() ] @@ -499,16 +499,13 @@ export async function generateReportForModule( * @param pkgJson package.json of the module. * * This function implements a fallback of locating module's bundle file. - * It first looks at esm2017 field of package.json, then module field. Main + * It first looks at the module field. Main * field at the last. * */ function retrieveBundleFileLocation(pkgJson: { [key: string]: string; }): string { - if (pkgJson['esm2017']) { - return pkgJson['esm2017']; - } if (pkgJson['module']) { return pkgJson['module']; } diff --git a/repo-scripts/size-analysis/bundle-definitions/firestore.json b/repo-scripts/size-analysis/bundle-definitions/firestore.json index 6c1adcad52c..f5ddafd167c 100644 --- a/repo-scripts/size-analysis/bundle-definitions/firestore.json +++ b/repo-scripts/size-analysis/bundle-definitions/firestore.json @@ -128,71 +128,6 @@ } ] }, - { - "name": "Pipeline Query with lt filter", - "dependencies": [ - { - "packageName": "firebase", - "versionOrTag": "latest", - "imports": [ - { - "path": "app", - "imports": [ - "initializeApp" - ] - } - ] - }, - { - "packageName": "firebase", - "versionOrTag": "latest", - "imports": [ - { - "path": "firestore", - "imports": [ - "getFirestore", - "lt", - "Field", - "useFirestorePipelines" - ] - } - ] - } - ] - }, - { - "name": "Pipeline Query with lt plus and function", - "dependencies": [ - { - "packageName": "firebase", - "versionOrTag": "latest", - "imports": [ - { - "path": "app", - "imports": [ - "initializeApp" - ] - } - ] - }, - { - "packageName": "firebase", - "versionOrTag": "latest", - "imports": [ - { - "path": "firestore", - "imports": [ - "getFirestore", - "lt", - "Field", - "useFirestorePipelines", - "andFunction" - ] - } - ] - } - ] - }, { "name": "Query Cursors", "dependencies": [ diff --git a/repo-scripts/size-analysis/bundle/rollup.ts b/repo-scripts/size-analysis/bundle/rollup.ts index 272cd934d08..250ecd8da06 100644 --- a/repo-scripts/size-analysis/bundle/rollup.ts +++ b/repo-scripts/size-analysis/bundle/rollup.ts @@ -32,7 +32,7 @@ export async function bundleWithRollup( moduleDirectory?: string ): Promise { const resolveOptions: RollupNodeResolveOptions = { - mainFields: ['esm2017', 'module', 'main'] + mainFields: ['module', 'main'] }; if (moduleDirectory) { diff --git a/repo-scripts/size-analysis/bundle/webpack.ts b/repo-scripts/size-analysis/bundle/webpack.ts index 611212b26b9..4f0598733ac 100644 --- a/repo-scripts/size-analysis/bundle/webpack.ts +++ b/repo-scripts/size-analysis/bundle/webpack.ts @@ -36,7 +36,7 @@ export async function bundleWithWebpack( const outputFileName = 'o.js'; const resolveConfig: webpack.ResolveOptions = { - mainFields: ['esm2017', 'module', 'main'] + mainFields: ['module', 'main'] }; if (moduleDirectory) { diff --git a/repo-scripts/size-analysis/package.json b/repo-scripts/size-analysis/package.json index 5084ee78d04..dd71cc9a378 100644 --- a/repo-scripts/size-analysis/package.json +++ b/repo-scripts/size-analysis/package.json @@ -5,7 +5,7 @@ "description": "A template package for new firebase packages", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "esm2017": "dist/index.esm2017.js", + "module": "dist/index.esm.js", "files": [ "dist" ], @@ -19,29 +19,31 @@ "build": "rollup -c" }, "license": "Apache-2.0", - "devDependencies": { - "@firebase/app": "0.13.1", - "@firebase/logger": "0.4.4", - "@firebase/util": "1.12.0", - "@rollup/plugin-commonjs": "21.1.0", - "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-node-resolve": "16.0.0", - "@rollup/plugin-virtual": "2.1.0", - "@types/webpack": "5.28.5", + "dependencies": { + "@firebase/util": "1.13.0", "child-process-promise": "2.2.1", "glob": "7.2.3", + "tmp": "0.2.3", "gzip-size": "6.0.0", "memfs": "3.5.3", + "terser": "5.37.0", + "@rollup/plugin-commonjs": "21.1.0", + "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-virtual": "2.1.0", "rollup": "2.79.2", "rollup-plugin-replace": "2.2.0", "rollup-plugin-typescript2": "0.36.0", - "terser": "5.37.0", - "tmp": "0.2.3", "typescript": "5.5.4", "webpack": "5.98.0", "webpack-virtual-modules": "0.6.2", "yargs": "17.7.2" }, + "devDependencies": { + "@firebase/app": "0.14.3", + "@firebase/logger": "0.5.0", + "@types/webpack": "5.28.5" + }, "repository": { "directory": "repo-scripts/size-analysis", "type": "git", @@ -57,6 +59,6 @@ "reportDir": "./coverage/node" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } -} +} \ No newline at end of file diff --git a/repo-scripts/size-analysis/rollup.config.js b/repo-scripts/size-analysis/rollup.config.js index 505596e3d2e..0be78dd920c 100644 --- a/repo-scripts/size-analysis/rollup.config.js +++ b/repo-scripts/size-analysis/rollup.config.js @@ -41,7 +41,7 @@ export default [ typescript, tsconfigOverride: { compilerOptions: { - target: 'es2017', + target: 'es2020', module: 'es2015' } } @@ -66,7 +66,7 @@ export default [ typescript, tsconfigOverride: { compilerOptions: { - target: 'es2017', + target: 'es2020', module: 'es2015' } } diff --git a/repo-scripts/size-analysis/tsconfig.json b/repo-scripts/size-analysis/tsconfig.json index 326e95a0fa6..acbfc91e7e5 100644 --- a/repo-scripts/size-analysis/tsconfig.json +++ b/repo-scripts/size-analysis/tsconfig.json @@ -5,7 +5,7 @@ "module": "commonjs", "moduleResolution": "node", "resolveJsonModule": true, - "target": "es2017", + "target": "es2020", "esModuleInterop": true, "declaration": true, "strict": true diff --git a/scripts/docgen/docgen.ts b/scripts/docgen/docgen.ts index 811570decd1..26045f84711 100644 --- a/scripts/docgen/docgen.ts +++ b/scripts/docgen/docgen.ts @@ -194,11 +194,23 @@ async function generateDocs( 'utf8' ); const authApiConfigModified = authApiConfigOriginal.replace( - `"mainEntryPointFilePath": "/dist/esm2017/index.d.ts"`, - `"mainEntryPointFilePath": "/dist/esm2017/index.doc.d.ts"` + `"mainEntryPointFilePath": "/dist/esm/index.d.ts"`, + `"mainEntryPointFilePath": "/dist/esm/index.doc.d.ts"` ); + /** + * Exclude compat as this script is only for modular docgen. + */ + const packageDirectories = ( + await mapWorkspaceToPackages([`${projectRoot}/packages/*`]) + ).filter(path => fs.existsSync(path) && !path.includes('-compat')); + try { + console.log(`Deleting old temp directories in each package.`); + for (const dir of packageDirectories) { + fs.rmSync(join(dir, 'temp'), { recursive: true, force: true }); + } + fs.writeFileSync( `${projectRoot}/packages/auth/api-extractor.json`, authApiConfigModified @@ -247,13 +259,9 @@ async function generateDocs( fs.mkdirSync(tmpDir); - // TODO: Throw error if path doesn't exist once all packages add markdown support. - const apiJsonDirectories = ( - await mapWorkspaceToPackages([`${projectRoot}/packages/*`]) - ) - .map(path => `${path}/temp`) + const apiJsonDirectories = packageDirectories + .map(path => join(path, 'temp')) .filter(path => fs.existsSync(path)); - for (const dir of apiJsonDirectories) { const paths = await new Promise(resolve => glob(`${dir}/*.api.json`, (err, paths) => { diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index 75f1741329a..5ff7e49ff96 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -8,7 +8,7 @@ "moduleResolution": "node", "esModuleInterop": true, "resolveJsonModule": true, - "target": "es2017", + "target": "es2020", "typeRoots": [ "../node_modules/@types" ], diff --git a/yarn.lock b/yarn.lock index 8e108902d5c..213ecd4d2c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -223,21 +223,11 @@ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== -"@babel/helper-string-parser@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" - integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== - "@babel/helper-validator-identifier@^7.25.9": version "7.25.9" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== -"@babel/helper-validator-identifier@^7.27.1": - version "7.27.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" - integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== - "@babel/helper-validator-option@^7.25.9": version "7.25.9" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" @@ -267,13 +257,6 @@ dependencies: "@babel/types" "^7.26.7" -"@babel/parser@^7.20.15": - version "7.27.7" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz#1687f5294b45039c159730e3b9c1f1b242e425e9" - integrity sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q== - dependencies: - "@babel/types" "^7.27.7" - "@babel/parser@^7.26.8": version "7.26.8" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz#deca2b4d99e5e1b1553843b99823f118da6107c2" @@ -1030,14 +1013,6 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@babel/types@^7.27.7": - version "7.27.7" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz#40eabd562049b2ee1a205fa589e629f945dce20f" - integrity sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw== - dependencies: - "@babel/helper-string-parser" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" - "@bazel/runfiles@^6.3.1": version "6.3.1" resolved "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.3.1.tgz#3f8824b2d82853377799d42354b4df78ab0ace0b" @@ -1613,13 +1588,6 @@ resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== -"@jsdoc/salty@^0.2.1": - version "0.2.9" - resolved "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz#4d8c147f7ca011532681ce86352a77a0178f1dec" - integrity sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw== - dependencies: - lodash "^4.17.21" - "@kwsites/file-exists@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99" @@ -3126,11 +3094,6 @@ dependencies: "@types/node" "*" -"@types/linkify-it@^5": - version "5.0.0" - resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" - integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== - "@types/listr@0.14.9": version "0.14.9" resolved "https://registry.npmjs.org/@types/listr/-/listr-0.14.9.tgz#736581cfdfcdb821bace0a3e5b05e91182e00c85" @@ -3144,19 +3107,6 @@ resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== -"@types/markdown-it@^14.1.1": - version "14.1.2" - resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" - integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== - dependencies: - "@types/linkify-it" "^5" - "@types/mdurl" "^2" - -"@types/mdurl@^2": - version "2.0.0" - resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" - integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== - "@types/mime@^1": version "1.3.5" resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" @@ -3817,16 +3767,6 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^5.0.0: - version "5.5.2" - resolved "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - integrity sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw== - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -4465,29 +4405,6 @@ b4a@^1.6.4: resolved "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g== - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-generator@^6.18.0: - version "6.26.1" - resolved "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - babel-loader@8.4.1: version "8.4.1" resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz#6ccb75c66e62c3b144e1c5f2eaec5b8f6c08c675" @@ -4498,13 +4415,6 @@ babel-loader@8.4.1: make-dir "^3.1.0" schema-utils "^2.6.5" -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w== - dependencies: - babel-runtime "^6.22.0" - babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -4561,55 +4471,6 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" -babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.16.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg== - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.18.0, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA== - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.18.0, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g== - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - bach@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880" @@ -4756,7 +4617,7 @@ blocking-proxy@^1.0.0: dependencies: minimist "^1.2.0" -bluebird@3.7.2, bluebird@^3.7.2: +bluebird@3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -5160,22 +5021,15 @@ camelcase@^6.0.0, camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001688: - version "1.0.30001695" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz#39dfedd8f94851132795fdf9b79d29659ad9c4d4" - integrity sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw== + version "1.0.30001731" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz" + integrity sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg== caseless@~0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -catharsis@^0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" - integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== - dependencies: - lodash "^4.17.15" - chai-as-promised@7.1.2: version "7.1.2" resolved "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz#70cd73b74afd519754161386421fb71832c6d041" @@ -5525,9 +5379,9 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -"closure-net@git+https://github.com/google/closure-net.git#0412666": +"closure-net@git+https://github.com/google/closure-net.git#6f48f578d3e80fe7a85e530a5d95b9351433d135": version "0.0.0" - resolved "git+https://github.com/google/closure-net.git#0412666e8f29b8ae69decb1fdc7ead635a5cf43e" + resolved "git+https://github.com/google/closure-net.git#6f48f578d3e80fe7a85e530a5d95b9351433d135" cmd-shim@^4.1.0: version "4.1.0" @@ -5536,11 +5390,6 @@ cmd-shim@^4.1.0: dependencies: mkdirp-infer-owner "^2.0.0" -co@^4.6.0: - version "4.6.0" - resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -5949,11 +5798,6 @@ core-js-compat@^3.40.0: dependencies: browserslist "^4.24.3" -core-js@^2.4.0: - version "2.6.12" - resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - core-util-is@1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -6195,7 +6039,7 @@ debug-fabulous@^1.0.0: memoizee "0.4.X" object-assign "4.X" -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -6302,7 +6146,7 @@ deep-freeze@0.0.1: resolved "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84" integrity sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg== -deep-is@^0.1.3, deep-is@~0.1.3: +deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -6462,13 +6306,6 @@ detect-file@^1.0.0: resolved "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A== - dependencies: - repeating "^2.0.0" - detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -6776,11 +6613,6 @@ ent@~2.2.0: punycode "^1.4.1" safe-regex-test "^1.1.0" -entities@^4.4.0: - version "4.5.0" - resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - env-paths@^2.2.0: version "2.2.1" resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -7009,18 +6841,6 @@ escape-string-regexp@^2.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escodegen@^1.13.0: - version "1.14.3" - resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" - integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== - dependencies: - esprima "^4.0.1" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - escodegen@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" @@ -7160,7 +6980,7 @@ esniff@^2.0.1: event-emitter "^0.3.5" type "^2.7.2" -espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -7188,7 +7008,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -7485,11 +7305,6 @@ fancy-log@^1.3.2, fancy-log@^1.3.3: parse-node-version "^1.0.0" time-stamp "^1.0.0" -fast-deep-equal@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" - integrity sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -7521,7 +7336,7 @@ fast-levenshtein@^1.0.0: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz#e6a754cc8f15e58987aa9cbd27af66fd6f4e5af9" integrity sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw== -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -8438,17 +8253,6 @@ glob@^10.0.0, glob@^10.2.2, glob@^10.3.10, glob@^10.4.1: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^8.0.0: - version "8.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - global-dirs@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" @@ -8488,11 +8292,6 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - globalthis@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" @@ -8615,7 +8414,7 @@ graceful-fs@4.2.10: resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.5, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.5, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -9298,13 +9097,6 @@ interpret@^1.0.0, interpret@^1.4.0: resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -invariant@^2.2.2: - version "2.2.4" - resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" @@ -9510,11 +9302,6 @@ is-finalizationregistry@^1.1.0: dependencies: call-bound "^1.0.3" -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" @@ -9946,21 +9733,6 @@ isstream@~0.1.2: resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== -istanbul-instrumenter-loader@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz#9957bd59252b373fae5c52b7b5188e6fde2a0949" - integrity sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w== - dependencies: - convert-source-map "^1.5.0" - istanbul-lib-instrument "^1.7.3" - loader-utils "^1.1.0" - schema-utils "^0.3.0" - -istanbul-lib-coverage@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" - integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ== - istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" @@ -9978,19 +9750,6 @@ istanbul-lib-hook@^3.0.0: dependencies: append-transform "^2.0.0" -istanbul-lib-instrument@^1.7.3: - version "1.10.2" - resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" - integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A== - dependencies: - babel-generator "^6.18.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - babylon "^6.18.0" - istanbul-lib-coverage "^1.2.1" - semver "^5.3.0" - istanbul-lib-instrument@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" @@ -10277,16 +10036,11 @@ jquery@^3.4.1: resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg== - js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -10310,13 +10064,6 @@ js-yaml@~3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js2xmlparser@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" - integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== - dependencies: - xmlcreate "^2.0.4" - jsbn@1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" @@ -10327,32 +10074,6 @@ jsbn@~0.1.0: resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== -jsdoc@^4.0.0: - version "4.0.4" - resolved "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz#86565a9e39cc723a3640465b3fb189a22d1206ca" - integrity sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw== - dependencies: - "@babel/parser" "^7.20.15" - "@jsdoc/salty" "^0.2.1" - "@types/markdown-it" "^14.1.1" - bluebird "^3.7.2" - catharsis "^0.9.0" - escape-string-regexp "^2.0.0" - js2xmlparser "^4.0.2" - klaw "^3.0.0" - markdown-it "^14.1.0" - markdown-it-anchor "^8.6.7" - marked "^4.0.10" - mkdirp "^1.0.4" - requizzle "^0.2.3" - strip-json-comments "^3.1.0" - underscore "~1.13.2" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA== - jsesc@^3.0.2: version "3.1.0" resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" @@ -10397,11 +10118,6 @@ json-ptr@^3.0.1: resolved "https://registry.npmjs.org/json-ptr/-/json-ptr-3.1.1.tgz#184c3d48db659fa9bbc1519f7db6f390ddffb659" integrity sha512-SiSJQ805W1sDUCD1+/t1/1BIrveq2Fe9HJqENxZmMCILmrPI7WhS/pePpIOx85v6/H2z1Vy7AI08GV2TzfXocg== -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - integrity sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA== - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -10438,7 +10154,7 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^1.0.1, json5@^1.0.2: +json5@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== @@ -10716,13 +10432,6 @@ klaw-sync@^6.0.0: dependencies: graceful-fs "^4.1.11" -klaw@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" - integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== - dependencies: - graceful-fs "^4.1.9" - kuler@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" @@ -10809,14 +10518,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - libnpmaccess@^4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-4.0.3.tgz#dfb0e5b0a53c315a2610d300e46b4ddeb66e7eec" @@ -10876,13 +10577,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-it@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" - integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== - dependencies: - uc.micro "^2.0.0" - listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" @@ -10963,15 +10657,6 @@ loader-runner@^4.2.0: resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -loader-utils@^1.1.0: - version "1.4.2" - resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" - integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - loader-utils@^2.0.0, loader-utils@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" @@ -11138,7 +10823,7 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" -lodash@4.17.21, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0, lodash@~4.17.15, lodash@~4.17.21: +lodash@4.17.21, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0, lodash@~4.17.15, lodash@~4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -11212,13 +10897,6 @@ long@^5.0.0: resolved "https://registry.npmjs.org/long/-/long-5.2.4.tgz#ee651d5c7c25901cfca5e67220ae9911695e99b2" integrity sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg== -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - loupe@^2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" @@ -11421,23 +11099,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-it-anchor@^8.6.7: - version "8.6.7" - resolved "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" - integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== - -markdown-it@^14.1.0: - version "14.1.0" - resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" - integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== - dependencies: - argparse "^2.0.1" - entities "^4.4.0" - linkify-it "^5.0.0" - mdurl "^2.0.0" - punycode.js "^2.3.1" - uc.micro "^2.1.0" - marked-terminal@^7.0.0: version "7.2.1" resolved "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.2.1.tgz#9c1ae073a245a03c6a13e3eeac6f586f29856068" @@ -11461,11 +11122,6 @@ marked@^13.0.2: resolved "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz#5c5b4a5d0198060c7c9bc6ef9420a7fed30f822d" integrity sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA== -marked@^4.0.10: - version "4.3.0" - resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" - integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== - matchdep@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e" @@ -11490,11 +11146,6 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -mdurl@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" - integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== - media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -11694,7 +11345,7 @@ minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatc dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@^5.1.0: +minimatch@^5.1.0: version "5.1.6" resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -12684,18 +12335,6 @@ optimist@~0.6.0: minimist "~0.0.1" wordwrap "~0.0.2" -optionator@^0.8.1: - version "0.8.3" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - optionator@^0.9.3: version "0.9.4" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -13487,11 +13126,6 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== - prettier@2.8.8, prettier@^2.7.1: version "2.8.8" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" @@ -13580,22 +13214,6 @@ proto3-json-serializer@^2.0.2: dependencies: protobufjs "^7.2.5" -protobufjs-cli@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz#c58b8566784f0fa1aff11e8d875a31de999637fe" - integrity sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ== - dependencies: - chalk "^4.0.0" - escodegen "^1.13.0" - espree "^9.0.0" - estraverse "^5.1.0" - glob "^8.0.0" - jsdoc "^4.0.0" - minimist "^1.2.0" - semver "^7.1.2" - tmp "^0.2.1" - uglify-js "^3.7.7" - protobufjs@7.4.0, protobufjs@^7.2.5, protobufjs@^7.3.2: version "7.4.0" resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" @@ -13726,11 +13344,6 @@ pumpify@^1.3.5: inherits "^2.0.3" pump "^2.0.0" -punycode.js@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" - integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== - punycode@^1.3.2, punycode@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -14113,11 +13726,6 @@ regenerate@^1.4.2: resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" @@ -14227,13 +13835,6 @@ repeat-string@^1.6.1: resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A== - dependencies: - is-finite "^1.0.0" - replace-ext@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" @@ -14308,13 +13909,6 @@ requires-port@^1.0.0: resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -requizzle@^0.2.3: - version "0.2.4" - resolved "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" - integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== - dependencies: - lodash "^4.17.21" - resolve-alpn@^1.0.0: version "1.2.1" resolved "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" @@ -14666,13 +14260,6 @@ sax@>=0.6.0: resolved "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== -schema-utils@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" - integrity sha512-QaVYBaD9U8scJw2EBWnCBY+LJ0AD+/2edTaigDs0XLDLBfJmSUK9KGqktg1rb32U3z4j/XwvFwHHH1YfbYFd7Q== - dependencies: - ajv "^5.0.0" - schema-utils@^2.6.5: version "2.7.1" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" @@ -14787,11 +14374,6 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semve resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== -semver@^7.1.2: - version "7.7.2" - resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" - integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== - semver@~7.3.0: version "7.3.8" resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" @@ -15251,7 +14833,7 @@ source-map-url@^0.4.0: resolved "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== -source-map@^0.5.6, source-map@^0.5.7: +source-map@^0.5.6: version "0.5.7" resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== @@ -15716,7 +15298,7 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1, strip-json-comments@~3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.1, strip-json-comments@~3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -16130,11 +15712,6 @@ to-absolute-glob@^2.0.0, to-absolute-glob@^2.0.2: is-absolute "^1.0.0" is-negated-glob "^1.0.0" -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og== - to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -16225,11 +15802,6 @@ trim-newlines@^3.0.0: resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw== - triple-beam@^1.3.0: version "1.4.1" resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" @@ -16363,13 +15935,6 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== - dependencies: - prelude-ls "~1.1.2" - type-detect@4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -16546,12 +16111,7 @@ ua-parser-js@^0.7.30: resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz#c87d83b7bb25822ecfa6397a0da5903934ea1562" integrity sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ== -uc.micro@^2.0.0, uc.micro@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" - integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== - -uglify-js@^3.1.4, uglify-js@^3.4.9, uglify-js@^3.7.7: +uglify-js@^3.1.4, uglify-js@^3.4.9: version "3.19.3" resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== @@ -16581,7 +16141,7 @@ unc-path-regex@^0.1.2: resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== -underscore@>=1.8.3, underscore@^1.9.1, underscore@~1.13.2: +underscore@>=1.8.3, underscore@^1.9.1: version "1.13.7" resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== @@ -17349,7 +16909,7 @@ winston@^3.0.0: triple-beam "^1.3.0" winston-transport "^4.9.0" -word-wrap@^1.2.5, word-wrap@~1.2.3: +word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== @@ -17528,11 +17088,6 @@ xmlbuilder@~11.0.0: resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== -xmlcreate@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" - integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== - xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"