diff --git a/.changeset/fluffy-rules-pretend.md b/.changeset/fluffy-rules-pretend.md new file mode 100644 index 00000000000..f0507222b35 --- /dev/null +++ b/.changeset/fluffy-rules-pretend.md @@ -0,0 +1,6 @@ +--- +'@firebase/app-check': patch +'@firebase/util': patch +--- + +Generate UUIDs with `crypto.randomUUID()` instead of custom uuidv4 function that uses `Math.random()`. diff --git a/.changeset/hip-apricots-end.md b/.changeset/hip-apricots-end.md new file mode 100644 index 00000000000..40bfedbaeec --- /dev/null +++ b/.changeset/hip-apricots-end.md @@ -0,0 +1,7 @@ +--- +'@firebase/remote-config-types': minor +'@firebase/remote-config': minor +'firebase': minor +--- + +Added support for custom signal targeting in Remote Config. Use `setCustomSignals` API for setting custom signals and use them to build custom targeting conditions in Remote Config. diff --git a/.changeset/tame-tigers-approve.md b/.changeset/tame-tigers-approve.md new file mode 100644 index 00000000000..42ce75ecfc0 --- /dev/null +++ b/.changeset/tame-tigers-approve.md @@ -0,0 +1,6 @@ +--- +"@firebase/data-connect": minor +"firebase": minor +--- + +Updated to include promise instead of promiselike diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a90241dea57..ec2416ecb23 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -37,10 +37,10 @@ packages/messaging-interop-types @zwu52 @firebase/jssdk-global-approvers integration/messaging @zwu52 @firebase/jssdk-global-approvers # Auth Code -packages/auth @lisajian @Xiaoshouzi-gh @sam-gc @firebase/jssdk-global-approvers -packages/auth-compat @lisajian @Xiaoshouzi-gh @sam-gc @firebase/jssdk-global-approvers -packages/auth-types @lisajian @Xiaoshouzi-gh @sam-gc @firebase/jssdk-global-approvers -packages/auth-interop-types @lisajian @Xiaoshouzi-gh @sam-gc @firebase/jssdk-global-approvers +packages/auth @lisajian @Xiaoshouzi-gh @sam-gc @pashanka @mansisampat @nhienlam @firebase/jssdk-global-approvers +packages/auth-compat @lisajian @Xiaoshouzi-gh @sam-gc @pashanka @mansisampat @nhienlam @firebase/jssdk-global-approvers +packages/auth-types @lisajian @Xiaoshouzi-gh @sam-gc @pashanka @mansisampat @nhienlam @firebase/jssdk-global-approvers +packages/auth-interop-types @lisajian @Xiaoshouzi-gh @sam-gc @pashanka @mansisampat @nhienlam @firebase/jssdk-global-approvers # Testing Code packages/rules-unit-testing @avolkovi @sam-gc @yuchenshi @firebase/jssdk-global-approvers diff --git a/.github/workflows/canary-deploy.yml b/.github/workflows/canary-deploy.yml index b2139d4df0c..2a9a6a803a6 100644 --- a/.github/workflows/canary-deploy.yml +++ b/.github/workflows/canary-deploy.yml @@ -32,7 +32,7 @@ jobs: # Canary release script requires git history and tags. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Yarn install diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml index fa93c2c6cc9..3514f409c81 100644 --- a/.github/workflows/check-changeset.yml +++ b/.github/workflows/check-changeset.yml @@ -35,7 +35,7 @@ jobs: # This makes Actions fetch all Git history so check_changeset script can diff properly. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Yarn install diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml index 34ad997fbc4..de182b3eeb0 100644 --- a/.github/workflows/check-docs.yml +++ b/.github/workflows/check-docs.yml @@ -28,7 +28,7 @@ jobs: # get all history for the diff fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Yarn install diff --git a/.github/workflows/check-pkg-paths.yml b/.github/workflows/check-pkg-paths.yml index 3d6fb1099e3..96dfc6f6556 100644 --- a/.github/workflows/check-pkg-paths.yml +++ b/.github/workflows/check-pkg-paths.yml @@ -28,7 +28,7 @@ jobs: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Yarn install diff --git a/.github/workflows/deploy-config.yml b/.github/workflows/deploy-config.yml index f01d8dcc9b3..0c3604e4704 100644 --- a/.github/workflows/deploy-config.yml +++ b/.github/workflows/deploy-config.yml @@ -36,7 +36,7 @@ jobs: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - name: Set up node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Yarn install diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index f22f77e0fe5..0857860571f 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -50,6 +50,12 @@ jobs: TEST_ACCOUNT: ${{ secrets.TEST_ACCOUNT }} run: | echo "export const config = $PROJECT_CONFIG; export const testAccount = $TEST_ACCOUNT" > firebase-config.js + - name: Poll npm until version to test is available for install + run: | + echo "Polling npm for firebase@${{ github.event.client_payload.versionOrTag }}" + node ../scripts/release/poll-npm-publish.js + env: + VERSION: ${{ github.event.client_payload.versionOrTag }} - name: Yarn install run: | echo "Installing firebase@${{ github.event.client_payload.versionOrTag }}" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 8dc971a1a8c..f0bbd672fc3 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -32,7 +32,7 @@ jobs: # get all history for the diff fetch-depth: 0 - name: Set up node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Yarn install diff --git a/.github/workflows/health-metrics-pull-request.yml b/.github/workflows/health-metrics-pull-request.yml index 8c7aba8fb50..ff7bd7286c1 100644 --- a/.github/workflows/health-metrics-pull-request.yml +++ b/.github/workflows/health-metrics-pull-request.yml @@ -38,8 +38,8 @@ jobs: if: (github.event_name == 'push' || !(github.event.pull_request.head.repo.fork)) && (github.actor != 'dependabot[bot]') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22.10.0 - uses: 'google-github-actions/auth@v0' @@ -55,8 +55,8 @@ jobs: if: (github.event_name == 'push' || !(github.event.pull_request.head.repo.fork)) && (github.actor != 'dependabot[bot]') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22.10.0 - uses: 'google-github-actions/auth@v0' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 82bb1410226..3ae2ae0a074 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,9 +22,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: yarn install diff --git a/.github/workflows/merge-release-branch.yml b/.github/workflows/merge-release-branch.yml index 19a3f6efd86..7142908b78a 100644 --- a/.github/workflows/merge-release-branch.yml +++ b/.github/workflows/merge-release-branch.yml @@ -37,7 +37,7 @@ jobs: run: | echo "Merging release ${{ steps.get-version.outputs.RELEASE_VERSION }}" - name: Merge to main - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/prerelease-manual-deploy.yml b/.github/workflows/prerelease-manual-deploy.yml index 7821734e85a..e5ccabdd144 100644 --- a/.github/workflows/prerelease-manual-deploy.yml +++ b/.github/workflows/prerelease-manual-deploy.yml @@ -35,7 +35,7 @@ jobs: # Canary release script requires git history and tags. fetch-depth: 0 - name: Set up node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Yarn install diff --git a/.github/workflows/release-prod.yml b/.github/workflows/release-prod.yml index 1ac9d1f3ee7..f5a5b808629 100644 --- a/.github/workflows/release-prod.yml +++ b/.github/workflows/release-prod.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Set up node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Checkout release branch (with history) diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index 94b66c6c7a5..c4adefb44a8 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -47,11 +47,11 @@ jobs: if: github.event.inputs.release-branch == 'release' || endsWith(github.event.inputs.release-branch, '-releasebranch') steps: - name: Set up node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Merge main into release - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.OSS_BOT_GITHUB_TOKEN }} script: | diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 68983f50a38..b7d40bc7245 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -39,9 +39,9 @@ jobs: - name: install Chrome stable run: | npx @puppeteer/browsers install chrome@stable - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Test setup and yarn install @@ -57,7 +57,7 @@ jobs: gzip build.tar - name: Upload build archive if: ${{ !cancelled() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build.tar.gz path: build.tar.gz @@ -74,13 +74,13 @@ jobs: run: | npx @puppeteer/browsers install chrome@stable - name: Download build archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Test setup and yarn install @@ -125,13 +125,13 @@ jobs: run: | echo $CHROME_VERSION_NOTES=$CHROME_VERSION_MISMATCH_MESSAGE - name: Download build archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Test setup and yarn install @@ -165,13 +165,13 @@ jobs: run: | npx @puppeteer/browsers install chrome@stable - name: Download build archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Test setup and yarn install @@ -209,13 +209,13 @@ jobs: run: | npx @puppeteer/browsers install chrome@stable - name: Download build archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build.tar.gz - name: Unzip build artifact run: tar xf build.tar.gz - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - run: cp config/ci.config.json config/project.json diff --git a/.github/workflows/test-changed-auth.yml b/.github/workflows/test-changed-auth.yml index e88ce3b7a52..445c2bfb9b8 100644 --- a/.github/workflows/test-changed-auth.yml +++ b/.github/workflows/test-changed-auth.yml @@ -57,7 +57,7 @@ jobs: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Test setup and yarn install @@ -82,7 +82,7 @@ jobs: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Test setup and yarn install diff --git a/.github/workflows/test-changed-fcm-integration.yml b/.github/workflows/test-changed-fcm-integration.yml index ee7b680f70d..ff6023274a4 100644 --- a/.github/workflows/test-changed-fcm-integration.yml +++ b/.github/workflows/test-changed-fcm-integration.yml @@ -39,7 +39,7 @@ jobs: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Test setup and yarn install diff --git a/.github/workflows/test-changed-firestore-integration.yml b/.github/workflows/test-changed-firestore-integration.yml index b288ea49ed0..d9269a6d1ac 100644 --- a/.github/workflows/test-changed-firestore-integration.yml +++ b/.github/workflows/test-changed-firestore-integration.yml @@ -69,7 +69,7 @@ jobs: continue-on-error: true - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: install Chrome stable diff --git a/.github/workflows/test-changed-firestore.yml b/.github/workflows/test-changed-firestore.yml index 259e9164d7f..e5a45745f7b 100644 --- a/.github/workflows/test-changed-firestore.yml +++ b/.github/workflows/test-changed-firestore.yml @@ -38,7 +38,7 @@ jobs: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: install Chrome stable @@ -72,7 +72,7 @@ jobs: gzip build.tar - name: Upload build archive if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.check-changed.outcome != 'success' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build.tar.gz path: build.tar.gz @@ -85,7 +85,7 @@ jobs: if: ${{ needs.build.outputs.changed == 'true'}} steps: - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: install Chrome stable @@ -93,7 +93,7 @@ jobs: sudo apt-get update sudo apt-get install google-chrome-stable - name: Download build archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build.tar.gz - name: Unzip build artifact @@ -113,7 +113,7 @@ jobs: if: ${{ needs.build.outputs.changed == 'true'}} steps: - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: install Chrome stable @@ -121,7 +121,7 @@ jobs: sudo apt-get update sudo apt-get install google-chrome-stable - name: Download build archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build.tar.gz - name: Unzip build artifact @@ -143,7 +143,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: install Chrome stable @@ -151,7 +151,7 @@ jobs: sudo apt-get update sudo apt-get install google-chrome-stable - name: Download build archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build.tar.gz - name: Unzip build artifact @@ -176,11 +176,11 @@ jobs: - name: install Firefox stable run: npx @puppeteer/browsers install firefox@stable - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Download build archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build.tar.gz - name: Unzip build artifact @@ -204,7 +204,7 @@ jobs: - name: install Firefox stable run: npx @puppeteer/browsers install firefox@stable - name: Download build archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build.tar.gz - name: Unzip build artifact @@ -265,7 +265,7 @@ jobs: - name: Unzip build artifact run: tar xf build.tar.gz - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Test setup diff --git a/.github/workflows/test-changed-misc.yml b/.github/workflows/test-changed-misc.yml index 3cabe64f2b3..ebcb2d1d366 100644 --- a/.github/workflows/test-changed-misc.yml +++ b/.github/workflows/test-changed-misc.yml @@ -32,7 +32,7 @@ jobs: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: install Chrome stable diff --git a/.github/workflows/test-changed.yml b/.github/workflows/test-changed.yml index 7dc0ef60a37..68a3e2b7830 100644 --- a/.github/workflows/test-changed.yml +++ b/.github/workflows/test-changed.yml @@ -32,7 +32,7 @@ jobs: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: install Chrome stable @@ -58,7 +58,7 @@ jobs: with: fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: install Firefox stable diff --git a/.github/workflows/test-firebase-integration.yml b/.github/workflows/test-firebase-integration.yml index 9a99e6f82a3..26a0cb2ead0 100644 --- a/.github/workflows/test-firebase-integration.yml +++ b/.github/workflows/test-firebase-integration.yml @@ -32,7 +32,7 @@ jobs: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - name: Set up Node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: install Chrome stable diff --git a/.github/workflows/update-api-reports.yml b/.github/workflows/update-api-reports.yml index 761d7020c7a..c961889de98 100644 --- a/.github/workflows/update-api-reports.yml +++ b/.github/workflows/update-api-reports.yml @@ -31,7 +31,7 @@ jobs: ref: ${{ github.event.pull_request.head.ref }} token: ${{ github.token }} - name: Set up node (20) - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 22.10.0 - name: Yarn install diff --git a/common/api-review/data-connect.api.md b/common/api-review/data-connect.api.md index b2c6fb01931..952d8b4dc10 100644 --- a/common/api-review/data-connect.api.md +++ b/common/api-review/data-connect.api.md @@ -11,14 +11,6 @@ import { FirebaseError } from '@firebase/util'; import { LogLevelString } from '@firebase/logger'; import { Provider } from '@firebase/component'; -// @public (undocumented) -export interface CancellableOperation extends PromiseLike<{ - data: T; -}> { - // (undocumented) - cancel: () => void; -} - // @public export function connectDataConnectEmulator(dc: DataConnect, host: string, port?: number, sslEnabled?: boolean): void; @@ -88,7 +80,7 @@ export function getDataConnect(app: FirebaseApp, options: ConnectorConfig): Data export const MUTATION_STR = "mutation"; // @public -export interface MutationPromise extends PromiseLike> { +export interface MutationPromise extends Promise> { } // @public (undocumented) @@ -144,7 +136,7 @@ export interface OpResult { export const QUERY_STR = "query"; // @public -export interface QueryPromise extends PromiseLike> { +export interface QueryPromise extends Promise> { } // @public diff --git a/common/api-review/remote-config.api.md b/common/api-review/remote-config.api.md index 980d8f3d287..bf6cf4761de 100644 --- a/common/api-review/remote-config.api.md +++ b/common/api-review/remote-config.api.md @@ -9,6 +9,12 @@ import { FirebaseApp } from '@firebase/app'; // @public export function activate(remoteConfig: RemoteConfig): Promise; +// @public +export interface CustomSignals { + // (undocumented) + [key: string]: string | number | null; +} + // @public export function ensureInitialized(remoteConfig: RemoteConfig): Promise; @@ -62,6 +68,9 @@ export interface RemoteConfigSettings { minimumFetchIntervalMillis: number; } +// @public +export function setCustomSignals(remoteConfig: RemoteConfig, customSignals: CustomSignals): Promise; + // @public export function setLogLevel(remoteConfig: RemoteConfig, logLevel: LogLevel): void; diff --git a/common/api-review/util.api.md b/common/api-review/util.api.md index 91d2f04cb40..8c62ff229ac 100644 --- a/common/api-review/util.api.md +++ b/common/api-review/util.api.md @@ -476,9 +476,6 @@ export interface Subscribe { // @public (undocumented) export type Unsubscribe = () => void; -// @public -export const uuidv4: () => string; - // Warning: (ae-missing-release-tag) "validateArgCount" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/docs-devsite/_toc.yaml b/docs-devsite/_toc.yaml index ca06d4f9398..4ab67bcd6ef 100644 --- a/docs-devsite/_toc.yaml +++ b/docs-devsite/_toc.yaml @@ -428,6 +428,8 @@ toc: - title: remote-config path: /docs/reference/js/remote-config.md section: + - title: CustomSignals + path: /docs/reference/js/remote-config.customsignals.md - title: RemoteConfig path: /docs/reference/js/remote-config.remoteconfig.md - title: RemoteConfigSettings diff --git a/docs-devsite/remote-config.customsignals.md b/docs-devsite/remote-config.customsignals.md new file mode 100644 index 00000000000..98bd371ad7b --- /dev/null +++ b/docs-devsite/remote-config.customsignals.md @@ -0,0 +1,23 @@ +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 %} + +# 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
+ +Signature: + +```typescript +export interface CustomSignals +``` diff --git a/docs-devsite/remote-config.md b/docs-devsite/remote-config.md index 371ab7ff157..40319453a3f 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. | +| [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() | | [isSupported()](./remote-config.md#issupported) | This method provides two different checks:1. Check if IndexedDB exists in the browser environment. 2. Check if the current browser context allows IndexedDB open() calls. | @@ -36,6 +37,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm | Interface | Description | | --- | --- | +| [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
| | [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) | The Firebase Remote Config service interface. | | [RemoteConfigSettings](./remote-config.remoteconfigsettings.md#remoteconfigsettings_interface) | Defines configuration options for the Remote Config SDK. | | [Value](./remote-config.value.md#value_interface) | Wraps a value with metadata and type-safe getters. | @@ -276,6 +278,27 @@ export declare function getValue(remoteConfig: RemoteConfig, key: string): Value The value for the given key. +### setCustomSignals(remoteConfig, customSignals) {:#setcustomsignals_aeeb95e} + +Sets the custom signals for the app instance. + +Signature: + +```typescript +export declare function setCustomSignals(remoteConfig: RemoteConfig, customSignals: CustomSignals): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| remoteConfig | [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) | The [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) instance. | +| customSignals | [CustomSignals](./remote-config.customsignals.md#customsignals_interface) | Map (key, value) of the custom signals to be set for the app instance. If a key already exists, the value is overwritten. Setting the value of a custom signal to null unsets the signal. The signals will be persisted locally on the client. | + +Returns: + +Promise<void> + ### setLogLevel(remoteConfig, logLevel) {:#setloglevel_039a45b} Defines the log level to use. diff --git a/packages/app-check/src/storage.ts b/packages/app-check/src/storage.ts index 3f7257e1945..36f34f00e16 100644 --- a/packages/app-check/src/storage.ts +++ b/packages/app-check/src/storage.ts @@ -16,7 +16,7 @@ */ import { FirebaseApp } from '@firebase/app'; -import { isIndexedDBAvailable, uuidv4 } from '@firebase/util'; +import { isIndexedDBAvailable } from '@firebase/util'; import { readDebugTokenFromIndexedDB, readTokenFromIndexedDB, @@ -77,7 +77,8 @@ export async function readOrCreateDebugTokenFromStorage(): Promise { if (!existingDebugToken) { // create a new debug token - const newToken = uuidv4(); + // This function is only available in secure contexts. See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts + const newToken = crypto.randomUUID(); // We don't need to block on writing to indexeddb // In case persistence failed, a new debug token will be generated every time the page is refreshed. // It renders the debug token useless because you have to manually register(whitelist) the new token in the firebase console again and again. diff --git a/packages/data-connect/src/api/Mutation.ts b/packages/data-connect/src/api/Mutation.ts index ca2efdb7a30..570b28b0eab 100644 --- a/packages/data-connect/src/api/Mutation.ts +++ b/packages/data-connect/src/api/Mutation.ts @@ -76,7 +76,7 @@ export function mutationRef( * @internal */ export class MutationManager { - private _inflight: Array> = []; + private _inflight: Array> = []; constructor(private _transport: DataConnectTransport) {} executeMutation( mutationRef: MutationRef @@ -95,7 +95,7 @@ export class MutationManager { return obj; }); this._inflight.push(result); - const removePromise = (): Array> => + const removePromise = (): Array> => (this._inflight = this._inflight.filter(promise => promise !== result)); result.then(removePromise, removePromise); return withRefPromise; @@ -113,7 +113,7 @@ export interface MutationResult * Mutation return value from `executeMutation` */ export interface MutationPromise - extends PromiseLike> { + extends Promise> { // reserved for special actions like cancellation } diff --git a/packages/data-connect/src/api/query.ts b/packages/data-connect/src/api/query.ts index a4ab17b7ceb..00382686dab 100644 --- a/packages/data-connect/src/api/query.ts +++ b/packages/data-connect/src/api/query.ts @@ -67,7 +67,7 @@ export interface QueryResult * Promise returned from `executeQuery` */ export interface QueryPromise - extends PromiseLike> { + extends Promise> { // reserved for special actions like cancellation } @@ -124,7 +124,7 @@ export function queryRef( dataConnect: dcInstance, refType: QUERY_STR, name: queryName, - variables: variables as Variables + variables: variables }; } /** diff --git a/packages/data-connect/src/network/fetch.ts b/packages/data-connect/src/network/fetch.ts index 06ef6dd8285..d5d2a439432 100644 --- a/packages/data-connect/src/network/fetch.ts +++ b/packages/data-connect/src/network/fetch.ts @@ -30,9 +30,14 @@ function getGoogApiClientValue(_isUsingGen: boolean): string { } return str; } +export interface DataConnectFetchBody { + name: string; + operationName: string; + variables: T; +} export function dcFetch( url: string, - body: U, + body: DataConnectFetchBody, { signal }: AbortController, appId: string | null, accessToken: string | null, @@ -95,7 +100,7 @@ export function dcFetch( logError('DataConnect error while performing request: ' + stringified); throw new DataConnectError(Code.OTHER, stringified); } - return res as { data: T; errors: Error[] }; + return res; }); } interface MessageObject { diff --git a/packages/data-connect/src/network/transport/index.ts b/packages/data-connect/src/network/transport/index.ts index 5518faa0f95..f4bb801f9b3 100644 --- a/packages/data-connect/src/network/transport/index.ts +++ b/packages/data-connect/src/network/transport/index.ts @@ -26,19 +26,15 @@ export interface DataConnectTransport { invokeQuery( queryName: string, body?: U - ): PromiseLike<{ data: T; errors: Error[] }>; + ): Promise<{ data: T; errors: Error[] }>; invokeMutation( queryName: string, body?: U - ): PromiseLike<{ data: T; errors: Error[] }>; + ): Promise<{ data: T; errors: Error[] }>; useEmulator(host: string, port?: number, sslEnabled?: boolean): void; onTokenChanged: (token: string | null) => void; } -export interface CancellableOperation extends PromiseLike<{ data: T }> { - cancel: () => void; -} - /** * @internal */ diff --git a/packages/data-connect/src/network/transport/rest.ts b/packages/data-connect/src/network/transport/rest.ts index 85847868c5d..0a49fc9e269 100644 --- a/packages/data-connect/src/network/transport/rest.ts +++ b/packages/data-connect/src/network/transport/rest.ts @@ -161,7 +161,7 @@ export class RESTTransport implements DataConnectTransport { invokeQuery: ( queryName: string, body?: U - ) => PromiseLike<{ data: T; errors: Error[] }> = ( + ) => Promise<{ data: T; errors: Error[] }> = ( queryName: string, body: U ) => { @@ -174,7 +174,7 @@ export class RESTTransport implements DataConnectTransport { name: `projects/${this._project}/locations/${this._location}/services/${this._serviceName}/connectors/${this._connectorName}`, operationName: queryName, variables: body - } as unknown as U, // TODO(mtewani): This is a patch, fix this. + }, abortController, this.appId, this._accessToken, @@ -182,16 +182,12 @@ export class RESTTransport implements DataConnectTransport { this._isUsingGen ) ); - - return { - then: withAuth.then.bind(withAuth), - catch: withAuth.catch.bind(withAuth) - }; + return withAuth; }; invokeMutation: ( queryName: string, body?: U - ) => PromiseLike<{ data: T; errors: Error[] }> = ( + ) => Promise<{ data: T; errors: Error[] }> = ( mutationName: string, body: U ) => { @@ -203,7 +199,7 @@ export class RESTTransport implements DataConnectTransport { name: `projects/${this._project}/locations/${this._location}/services/${this._serviceName}/connectors/${this._connectorName}`, operationName: mutationName, variables: body - } as unknown as U, + }, abortController, this.appId, this._accessToken, @@ -211,12 +207,6 @@ export class RESTTransport implements DataConnectTransport { this._isUsingGen ); }); - - return { - then: taskResult.then.bind(taskResult), - // catch: taskResult.catch.bind(taskResult), - // finally: taskResult.finally.bind(taskResult), - cancel: () => abortController.abort() - }; + return taskResult; }; } diff --git a/packages/data-connect/test/queries.test.ts b/packages/data-connect/test/queries.test.ts index dd7e4e6c9e3..8b630242a4e 100644 --- a/packages/data-connect/test/queries.test.ts +++ b/packages/data-connect/test/queries.test.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { uuidv4 } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; @@ -51,11 +50,11 @@ interface TaskListResponse { const SEEDED_DATA = [ { - id: uuidv4(), + id: crypto.randomUUID(), content: 'task 1' }, { - id: uuidv4(), + id: crypto.randomUUID(), content: 'task 2' } ]; diff --git a/packages/data-connect/test/unit/fetch.test.ts b/packages/data-connect/test/unit/fetch.test.ts index a50ac188724..3d9a9b04523 100644 --- a/packages/data-connect/test/unit/fetch.test.ts +++ b/packages/data-connect/test/unit/fetch.test.ts @@ -42,7 +42,11 @@ describe('fetch', () => { await expect( dcFetch( 'http://localhost', - {}, + { + name: 'n', + operationName: 'n', + variables: {} + }, {} as AbortController, null, null, @@ -61,7 +65,11 @@ describe('fetch', () => { await expect( dcFetch( 'http://localhost', - {}, + { + name: 'n', + operationName: 'n', + variables: {} + }, {} as AbortController, null, null, diff --git a/packages/database/test/helpers/util.ts b/packages/database/test/helpers/util.ts index 91c627c9a14..73eb04a8c5e 100644 --- a/packages/database/test/helpers/util.ts +++ b/packages/database/test/helpers/util.ts @@ -16,7 +16,6 @@ */ import { FirebaseApp, initializeApp } from '@firebase/app'; -import { uuidv4 } from '@firebase/util'; import { expect } from 'chai'; import { @@ -105,7 +104,7 @@ export function waitFor(waitTimeInMS: number) { // Creates a unique reference using uuid export function getUniqueRef(db: Database) { - const path = uuidv4(); + const path = crypto.randomUUID(); return ref(db, path); } diff --git a/packages/firebase/compat/index.d.ts b/packages/firebase/compat/index.d.ts index efda7c954a5..92c7bd2c278 100644 --- a/packages/firebase/compat/index.d.ts +++ b/packages/firebase/compat/index.d.ts @@ -2046,6 +2046,7 @@ declare namespace firebase.remoteConfig { * Defines levels of Remote Config logging. */ export type LogLevel = 'debug' | 'error' | 'silent'; + /** * This method provides two different checks: * diff --git a/packages/firestore/src/local/memory_remote_document_cache.ts b/packages/firestore/src/local/memory_remote_document_cache.ts index 2b145acdf9d..42a0010d4ac 100644 --- a/packages/firestore/src/local/memory_remote_document_cache.ts +++ b/packages/firestore/src/local/memory_remote_document_cache.ts @@ -47,6 +47,11 @@ interface MemoryRemoteDocumentCacheEntry { size: number; } +/** + * The smallest value representable by a 64-bit signed integer (long). + */ +const MIN_LONG_VALUE = '-9223372036854775808'; + type DocumentEntryMap = SortedMap; function documentEntryMap(): DocumentEntryMap { return new SortedMap( @@ -171,7 +176,12 @@ class MemoryRemoteDocumentCacheImpl implements MemoryRemoteDocumentCache { // Documents are ordered by key, so we can use a prefix scan to narrow down // the documents we need to match the query against. const collectionPath = query.path; - const prefix = new DocumentKey(collectionPath.child('')); + // Document keys are ordered first by numeric value ("__id__"), + // then lexicographically by string value. Start the iterator at the minimum + // possible Document key value. + const prefix = new DocumentKey( + collectionPath.child('__id' + MIN_LONG_VALUE + '__') + ); const iterator = this.docs.getIteratorFrom(prefix); while (iterator.hasNext()) { const { diff --git a/packages/firestore/src/model/path.ts b/packages/firestore/src/model/path.ts index 3b68a67c68f..64cb0376a0e 100644 --- a/packages/firestore/src/model/path.ts +++ b/packages/firestore/src/model/path.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { Integer } from '@firebase/webchannel-wrapper/bloom-blob'; + import { debugAssert, fail } from '../util/assert'; import { Code, FirestoreError } from '../util/error'; @@ -163,28 +165,59 @@ abstract class BasePath> { return this.segments.slice(this.offset, this.limit()); } + /** + * Compare 2 paths segment by segment, prioritizing numeric IDs + * (e.g., "__id123__") in numeric ascending order, followed by string + * segments in lexicographical order. + */ static comparator>( p1: BasePath, p2: BasePath ): number { const len = Math.min(p1.length, p2.length); for (let i = 0; i < len; i++) { - const left = p1.get(i); - const right = p2.get(i); - if (left < right) { - return -1; - } - if (left > right) { - return 1; + const comparison = BasePath.compareSegments(p1.get(i), p2.get(i)); + if (comparison !== 0) { + return comparison; } } - if (p1.length < p2.length) { + return Math.sign(p1.length - p2.length); + } + + private static compareSegments(lhs: string, rhs: string): number { + const isLhsNumeric = BasePath.isNumericId(lhs); + const isRhsNumeric = BasePath.isNumericId(rhs); + + if (isLhsNumeric && !isRhsNumeric) { + // Only lhs is numeric return -1; - } - if (p1.length > p2.length) { + } else if (!isLhsNumeric && isRhsNumeric) { + // Only rhs is numeric return 1; + } else if (isLhsNumeric && isRhsNumeric) { + // both numeric + return BasePath.extractNumericId(lhs).compare( + BasePath.extractNumericId(rhs) + ); + } else { + // both non-numeric + if (lhs < rhs) { + return -1; + } + if (lhs > rhs) { + return 1; + } + return 0; } - return 0; + } + + // Checks if a segment is a numeric ID (starts with "__id" and ends with "__"). + private static isNumericId(segment: string): boolean { + return segment.startsWith('__id') && segment.endsWith('__'); + } + + private static extractNumericId(segment: string): Integer { + return Integer.fromString(segment.substring(4, segment.length - 2)); } } diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 81dc7362a22..1cda49d9229 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -79,7 +79,8 @@ import { withTestDocAndInitialData, withNamedTestDbsOrSkipUnlessUsingEmulator, toDataArray, - checkOnlineAndOfflineResultsMatch + checkOnlineAndOfflineResultsMatch, + toIds } from '../util/helpers'; import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from '../util/settings'; @@ -2245,4 +2246,182 @@ apiDescribe('Database', persistence => { }); }); }); + + describe('sort documents by DocumentId', () => { + it('snapshot listener sorts query by DocumentId same way as get query', async () => { + const testDocs = { + 'A': { a: 1 }, + 'a': { a: 1 }, + 'Aa': { a: 1 }, + '7': { a: 1 }, + '12': { a: 1 }, + '__id7__': { a: 1 }, + '__id12__': { a: 1 }, + '__id-2__': { a: 1 }, + '_id1__': { a: 1 }, + '__id1_': { a: 1 }, + '__id': { a: 1 }, + // largest long numbers + '__id9223372036854775807__': { a: 1 }, + '__id9223372036854775806__': { a: 1 }, + // smallest long numbers + '__id-9223372036854775808__': { a: 1 }, + '__id-9223372036854775807__': { a: 1 } + }; + + return withTestCollection(persistence, testDocs, async collectionRef => { + const orderedQuery = query(collectionRef, orderBy(documentId())); + const expectedDocs = [ + '__id-9223372036854775808__', + '__id-9223372036854775807__', + '__id-2__', + '__id7__', + '__id12__', + '__id9223372036854775806__', + '__id9223372036854775807__', + '12', + '7', + 'A', + 'Aa', + '__id', + '__id1_', + '_id1__', + 'a' + ]; + + const getSnapshot = await getDocsFromServer(orderedQuery); + expect(toIds(getSnapshot)).to.deep.equal(expectedDocs); + + const storeEvent = new EventsAccumulator(); + const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent); + const watchSnapshot = await storeEvent.awaitEvent(); + expect(toIds(watchSnapshot)).to.deep.equal(expectedDocs); + + unsubscribe(); + }); + }); + + it('snapshot listener sorts filtered query by DocumentId same way as get query', async () => { + const testDocs = { + 'A': { a: 1 }, + 'a': { a: 1 }, + 'Aa': { a: 1 }, + '7': { a: 1 }, + '12': { a: 1 }, + '__id7__': { a: 1 }, + '__id12__': { a: 1 }, + '__id-2__': { a: 1 }, + '_id1__': { a: 1 }, + '__id1_': { a: 1 }, + '__id': { a: 1 }, + // largest long numbers + '__id9223372036854775807__': { a: 1 }, + '__id9223372036854775806__': { a: 1 }, + // smallest long numbers + '__id-9223372036854775808__': { a: 1 }, + '__id-9223372036854775807__': { a: 1 } + }; + + return withTestCollection(persistence, testDocs, async collectionRef => { + const filteredQuery = query( + collectionRef, + orderBy(documentId()), + where(documentId(), '>', '__id7__'), + where(documentId(), '<=', 'Aa') + ); + const expectedDocs = [ + '__id12__', + '__id9223372036854775806__', + '__id9223372036854775807__', + '12', + '7', + 'A', + 'Aa' + ]; + + const getSnapshot = await getDocsFromServer(filteredQuery); + expect(toIds(getSnapshot)).to.deep.equal(expectedDocs); + + const storeEvent = new EventsAccumulator(); + const unsubscribe = onSnapshot(filteredQuery, storeEvent.storeEvent); + const watchSnapshot = await storeEvent.awaitEvent(); + expect(toIds(watchSnapshot)).to.deep.equal(expectedDocs); + unsubscribe(); + }); + }); + + // eslint-disable-next-line no-restricted-properties + (persistence.gc === 'lru' ? describe : describe.skip)('offline', () => { + it('SDK orders query the same way online and offline', async () => { + const testDocs = { + 'A': { a: 1 }, + 'a': { a: 1 }, + 'Aa': { a: 1 }, + '7': { a: 1 }, + '12': { a: 1 }, + '__id7__': { a: 1 }, + '__id12__': { a: 1 }, + '__id-2__': { a: 1 }, + '_id1__': { a: 1 }, + '__id1_': { a: 1 }, + '__id': { a: 1 }, + // largest long numbers + '__id9223372036854775807__': { a: 1 }, + '__id9223372036854775806__': { a: 1 }, + // smallest long numbers + '__id-9223372036854775808__': { a: 1 }, + '__id-9223372036854775807__': { a: 1 } + }; + + return withTestCollection( + persistence, + testDocs, + async collectionRef => { + const orderedQuery = query(collectionRef, orderBy(documentId())); + let expectedDocs = [ + '__id-9223372036854775808__', + '__id-9223372036854775807__', + '__id-2__', + '__id7__', + '__id12__', + '__id9223372036854775806__', + '__id9223372036854775807__', + '12', + '7', + 'A', + 'Aa', + '__id', + '__id1_', + '_id1__', + 'a' + ]; + await checkOnlineAndOfflineResultsMatch( + orderedQuery, + ...expectedDocs + ); + + const filteredQuery = query( + collectionRef, + orderBy(documentId()), + where(documentId(), '>', '__id7__'), + where(documentId(), '<=', 'Aa') + ); + expectedDocs = [ + '__id12__', + '__id9223372036854775806__', + '__id9223372036854775807__', + '12', + '7', + 'A', + 'Aa' + ]; + await checkOnlineAndOfflineResultsMatch( + filteredQuery, + ...expectedDocs + ); + } + ); + }); + }); + }); }); diff --git a/packages/remote-config-types/index.d.ts b/packages/remote-config-types/index.d.ts index a088f665310..7fbaf7c3e5c 100644 --- a/packages/remote-config-types/index.d.ts +++ b/packages/remote-config-types/index.d.ts @@ -173,6 +173,19 @@ export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle'; */ export type LogLevel = 'debug' | 'error' | 'silent'; +/** + * Defines the type for representing custom signals and their values. + * + *

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

    + *
  • string + *
  • number + *
  • null + *
+ */ +export type CustomSignals = { [key: string]: string | number | null }; + declare module '@firebase/component' { interface NameServiceMapping { 'remoteConfig-compat': RemoteConfig; diff --git a/packages/remote-config/src/api.ts b/packages/remote-config/src/api.ts index aeae67d450e..607d4944d26 100644 --- a/packages/remote-config/src/api.ts +++ b/packages/remote-config/src/api.ts @@ -17,12 +17,17 @@ import { _getProvider, FirebaseApp, getApp } from '@firebase/app'; import { + CustomSignals, LogLevel as RemoteConfigLogLevel, RemoteConfig, Value } from './public_types'; import { RemoteConfigAbortSignal } from './client/remote_config_fetch_client'; -import { RC_COMPONENT_NAME } from './constants'; +import { + RC_COMPONENT_NAME, + RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH, + RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH +} from './constants'; import { ErrorCode, hasErrorCode } from './errors'; import { RemoteConfig as RemoteConfigImpl } from './remote_config'; import { Value as ValueImpl } from './value'; @@ -114,11 +119,18 @@ export async function fetchConfig(remoteConfig: RemoteConfig): Promise { abortSignal.abort(); }, rc.settings.fetchTimeoutMillis); + const customSignals = rc._storageCache.getCustomSignals(); + if (customSignals) { + rc._logger.debug( + `Fetching config with custom signals: ${JSON.stringify(customSignals)}` + ); + } // Catches *all* errors thrown by client so status can be set consistently. try { await rc._client.fetch({ cacheMaxAgeMillis: rc.settings.minimumFetchIntervalMillis, - signal: abortSignal + signal: abortSignal, + customSignals }); await rc._storageCache.setLastFetchStatus('success'); @@ -258,3 +270,51 @@ export function setLogLevel( function getAllKeys(obj1: {} = {}, obj2: {} = {}): string[] { return Object.keys({ ...obj1, ...obj2 }); } + +/** + * Sets the custom signals for the app instance. + * + * @param remoteConfig - The {@link RemoteConfig} instance. + * @param customSignals - Map (key, value) of the custom signals to be set for the app instance. If + * a key already exists, the value is overwritten. Setting the value of a custom signal to null + * unsets the signal. The signals will be persisted locally on the client. + * + * @public + */ +export async function setCustomSignals( + remoteConfig: RemoteConfig, + customSignals: CustomSignals +): Promise { + const rc = getModularInstance(remoteConfig) as RemoteConfigImpl; + if (Object.keys(customSignals).length === 0) { + return; + } + + // eslint-disable-next-line guard-for-in + for (const key in customSignals) { + if (key.length > RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH) { + rc._logger.error( + `Custom signal key ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH}.` + ); + return; + } + const value = customSignals[key]; + if ( + typeof value === 'string' && + value.length > RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH + ) { + rc._logger.error( + `Value supplied for custom signal ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH}.` + ); + return; + } + } + + try { + await rc._storageCache.setCustomSignals(customSignals); + } catch (error) { + rc._logger.error( + `Error encountered while setting custom signals: ${error}` + ); + } +} 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 25e00299855..71ea66d5e50 100644 --- a/packages/remote-config/src/client/remote_config_fetch_client.ts +++ b/packages/remote-config/src/client/remote_config_fetch_client.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { CustomSignals } from '../public_types'; + /** * Defines a client, as in https://en.wikipedia.org/wiki/Client%E2%80%93server_model, for the * Remote Config server (https://firebase.google.com/docs/reference/remote-config/rest). @@ -99,6 +101,12 @@ export interface FetchRequest { *

Comparable to passing `headers = { 'If-None-Match': }` to the native Fetch API. */ eTag?: string; + + /** The custom signals stored for the app instance. + * + *

Optional in case no custom signals are set for the instance. + */ + customSignals?: CustomSignals; } /** diff --git a/packages/remote-config/src/client/rest_client.ts b/packages/remote-config/src/client/rest_client.ts index 87fdae3c3d6..9d87ffbb1ac 100644 --- a/packages/remote-config/src/client/rest_client.ts +++ b/packages/remote-config/src/client/rest_client.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { CustomSignals } from '../public_types'; import { FetchResponse, RemoteConfigFetchClient, @@ -41,6 +42,7 @@ interface FetchRequestBody { app_instance_id_token: string; app_id: string; language_code: string; + custom_signals?: CustomSignals; /* eslint-enable camelcase */ } @@ -92,7 +94,8 @@ export class RestClient implements RemoteConfigFetchClient { app_instance_id: installationId, app_instance_id_token: installationToken, app_id: this.appId, - language_code: getUserLanguage() + language_code: getUserLanguage(), + custom_signals: request.customSignals /* eslint-enable camelcase */ }; diff --git a/packages/remote-config/src/constants.ts b/packages/remote-config/src/constants.ts index 365d9037f86..d7d286909a5 100644 --- a/packages/remote-config/src/constants.ts +++ b/packages/remote-config/src/constants.ts @@ -16,3 +16,6 @@ */ export const RC_COMPONENT_NAME = 'remote-config'; +export const RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 100; +export const RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH = 250; +export const RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH = 500; diff --git a/packages/remote-config/src/errors.ts b/packages/remote-config/src/errors.ts index eac9a25657b..762eeb899ee 100644 --- a/packages/remote-config/src/errors.ts +++ b/packages/remote-config/src/errors.ts @@ -31,7 +31,8 @@ export const enum ErrorCode { FETCH_THROTTLE = 'fetch-throttle', FETCH_PARSE = 'fetch-client-parse', FETCH_STATUS = 'fetch-status', - INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable' + INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable', + CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 'custom-signal-max-allowed-signals' } const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { @@ -67,7 +68,9 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { [ErrorCode.FETCH_STATUS]: 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.', [ErrorCode.INDEXED_DB_UNAVAILABLE]: - 'Indexed DB is not supported by current browser' + 'Indexed DB is not supported by current browser', + [ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS]: + 'Setting more than {$maxSignals} custom signals is not supported.' }; // Note this is effectively a type system binding a code to params. This approach overlaps with the @@ -86,6 +89,7 @@ interface ErrorParams { [ErrorCode.FETCH_THROTTLE]: { throttleEndTimeMillis: number }; [ErrorCode.FETCH_PARSE]: { originalErrorMessage: string }; [ErrorCode.FETCH_STATUS]: { httpStatus: number }; + [ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS]: { maxSignals: number }; } 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 d489809e451..365d5e5905f 100644 --- a/packages/remote-config/src/public_types.ts +++ b/packages/remote-config/src/public_types.ts @@ -134,6 +134,23 @@ export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle'; */ export type LogLevel = 'debug' | 'error' | 'silent'; +/** + * Defines the type for representing custom signals and their values. + * + *

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

    + *
  • string + *
  • number + *
  • null + *
+ * + * @public + */ +export interface CustomSignals { + [key: string]: string | number | null; +} + declare module '@firebase/component' { interface NameServiceMapping { 'remote-config': RemoteConfig; diff --git a/packages/remote-config/src/storage/storage.ts b/packages/remote-config/src/storage/storage.ts index baa7ab46b52..52e660f1fdb 100644 --- a/packages/remote-config/src/storage/storage.ts +++ b/packages/remote-config/src/storage/storage.ts @@ -15,12 +15,13 @@ * limitations under the License. */ -import { FetchStatus } from '@firebase/remote-config-types'; +import { FetchStatus, CustomSignals } from '@firebase/remote-config-types'; import { FetchResponse, FirebaseRemoteConfigObject } from '../client/remote_config_fetch_client'; import { ERROR_FACTORY, ErrorCode } from '../errors'; +import { RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS } from '../constants'; import { FirebaseError } from '@firebase/util'; /** @@ -70,7 +71,8 @@ type ProjectNamespaceKeyFieldValue = | 'last_successful_fetch_timestamp_millis' | 'last_successful_fetch_response' | 'settings' - | 'throttle_metadata'; + | 'throttle_metadata' + | 'custom_signals'; // Visible for testing. export function openDatabase(): Promise { @@ -181,10 +183,64 @@ export class Storage { return this.delete('throttle_metadata'); } - async get(key: ProjectNamespaceKeyFieldValue): Promise { + getCustomSignals(): Promise { + return this.get('custom_signals'); + } + + async setCustomSignals(customSignals: CustomSignals): Promise { const db = await this.openDbPromise; + const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite'); + const storedSignals = await this.getWithTransaction( + 'custom_signals', + transaction + ); + const combinedSignals = { + ...storedSignals, + ...customSignals + }; + // Filter out key-value assignments with null values since they are signals being unset + const updatedSignals = Object.fromEntries( + Object.entries(combinedSignals) + .filter(([_, v]) => v !== null) + .map(([k, v]) => { + // Stringify numbers to store a map of string keys and values which can be sent + // as-is in a fetch call. + if (typeof v === 'number') { + return [k, v.toString()]; + } + return [k, v]; + }) + ); + + // Throw an error if the number of custom signals to be stored exceeds the limit + if ( + Object.keys(updatedSignals).length > RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS + ) { + throw ERROR_FACTORY.create(ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS, { + maxSignals: RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS + }); + } + + await this.setWithTransaction( + 'custom_signals', + updatedSignals, + transaction + ); + return updatedSignals; + } + + /** + * Gets a value from the database using the provided transaction. + * + * @param key The key of the value to get. + * @param transaction The transaction to use for the operation. + * @returns The value associated with the key, or undefined if no such value exists. + */ + async getWithTransaction( + key: ProjectNamespaceKeyFieldValue, + transaction: IDBTransaction + ): Promise { return new Promise((resolve, reject) => { - const transaction = db.transaction([APP_NAMESPACE_STORE], 'readonly'); const objectStore = transaction.objectStore(APP_NAMESPACE_STORE); const compositeKey = this.createCompositeKey(key); try { @@ -210,10 +266,20 @@ export class Storage { }); } - async set(key: ProjectNamespaceKeyFieldValue, value: T): Promise { - const db = await this.openDbPromise; + /** + * Sets a value in the database using the provided transaction. + * + * @param key The key of the value to set. + * @param value The value to set. + * @param transaction The transaction to use for the operation. + * @returns A promise that resolves when the operation is complete. + */ + async setWithTransaction( + key: ProjectNamespaceKeyFieldValue, + value: T, + transaction: IDBTransaction + ): Promise { return new Promise((resolve, reject) => { - const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite'); const objectStore = transaction.objectStore(APP_NAMESPACE_STORE); const compositeKey = this.createCompositeKey(key); try { @@ -237,6 +303,18 @@ export class Storage { }); } + async get(key: ProjectNamespaceKeyFieldValue): Promise { + const db = await this.openDbPromise; + const transaction = db.transaction([APP_NAMESPACE_STORE], 'readonly'); + return this.getWithTransaction(key, transaction); + } + + async set(key: ProjectNamespaceKeyFieldValue, value: T): Promise { + const db = await this.openDbPromise; + const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite'); + return this.setWithTransaction(key, value, transaction); + } + async delete(key: ProjectNamespaceKeyFieldValue): Promise { const db = await this.openDbPromise; return new Promise((resolve, reject) => { diff --git a/packages/remote-config/src/storage/storage_cache.ts b/packages/remote-config/src/storage/storage_cache.ts index 302ba9a2487..fc419b0068e 100644 --- a/packages/remote-config/src/storage/storage_cache.ts +++ b/packages/remote-config/src/storage/storage_cache.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { FetchStatus } from '@firebase/remote-config-types'; +import { FetchStatus, CustomSignals } from '@firebase/remote-config-types'; import { FirebaseRemoteConfigObject } from '../client/remote_config_fetch_client'; import { Storage } from './storage'; @@ -31,6 +31,7 @@ export class StorageCache { private lastFetchStatus?: FetchStatus; private lastSuccessfulFetchTimestampMillis?: number; private activeConfig?: FirebaseRemoteConfigObject; + private customSignals?: CustomSignals; /** * Memory-only getters @@ -47,6 +48,10 @@ export class StorageCache { return this.activeConfig; } + getCustomSignals(): CustomSignals | undefined { + return this.customSignals; + } + /** * Read-ahead getter */ @@ -55,6 +60,7 @@ export class StorageCache { const lastSuccessfulFetchTimestampMillisPromise = this.storage.getLastSuccessfulFetchTimestampMillis(); const activeConfigPromise = this.storage.getActiveConfig(); + const customSignalsPromise = this.storage.getCustomSignals(); // Note: // 1. we consistently check for undefined to avoid clobbering defined values @@ -78,6 +84,11 @@ export class StorageCache { if (activeConfig) { this.activeConfig = activeConfig; } + + const customSignals = await customSignalsPromise; + if (customSignals) { + this.customSignals = customSignals; + } } /** @@ -99,4 +110,8 @@ export class StorageCache { this.activeConfig = activeConfig; return this.storage.setActiveConfig(activeConfig); } + + async setCustomSignals(customSignals: CustomSignals): Promise { + this.customSignals = await this.storage.setCustomSignals(customSignals); + } } diff --git a/packages/remote-config/test/remote_config.test.ts b/packages/remote-config/test/remote_config.test.ts index f53f4a72c86..51304bc3b2f 100644 --- a/packages/remote-config/test/remote_config.test.ts +++ b/packages/remote-config/test/remote_config.test.ts @@ -42,7 +42,8 @@ import { getString, getValue, setLogLevel, - fetchConfig + fetchConfig, + setCustomSignals } from '../src/api'; import * as api from '../src/api'; import { fetchAndActivate } from '../src'; @@ -93,6 +94,48 @@ describe('RemoteConfig', () => { loggerLogLevelSpy.restore(); }); + describe('setCustomSignals', () => { + beforeEach(() => { + storageCache.setCustomSignals = sinon.stub(); + storage.setCustomSignals = sinon.stub(); + logger.error = sinon.stub(); + }); + + it('call storage API to store signals', async () => { + await setCustomSignals(rc, { key: 'value' }); + + expect(storageCache.setCustomSignals).to.have.been.calledWith({ + key: 'value' + }); + }); + + it('logs an error when supplied with a custom signal key greater than 250 characters', async () => { + const longKey = 'a'.repeat(251); + const customSignals = { [longKey]: 'value' }; + + await setCustomSignals(rc, customSignals); + + expect(storageCache.setCustomSignals).to.not.have.been.called; + expect(logger.error).to.have.been.called; + }); + + it('logs an error when supplied with a custom signal value greater than 500 characters', async () => { + const longValue = 'a'.repeat(501); + const customSignals = { 'key': longValue }; + + await setCustomSignals(rc, customSignals); + + expect(storageCache.setCustomSignals).to.not.have.been.called; + expect(logger.error).to.have.been.called; + }); + + it('empty custom signals map does nothing', async () => { + await setCustomSignals(rc, {}); + + expect(storageCache.setCustomSignals).to.not.have.been.called; + }); + }); + // Adapts getUserLanguage tests from packages/auth/test/utils_test.js for TypeScript. describe('setLogLevel', () => { it('proxies to the FirebaseLogger instance', () => { @@ -449,6 +492,7 @@ describe('RemoteConfig', () => { .stub() .returns(Promise.resolve({ status: 200 } as FetchResponse)); storageCache.setLastFetchStatus = sinon.stub(); + storageCache.getCustomSignals = sinon.stub(); timeoutStub = sinon.stub(window, 'setTimeout'); }); @@ -517,5 +561,11 @@ describe('RemoteConfig', () => { 'failure' ); }); + + it('sends custom signals', async () => { + await fetchConfig(rc); + + expect(storageCache.getCustomSignals).to.have.been.called; + }); }); }); diff --git a/packages/remote-config/test/storage/storage.test.ts b/packages/remote-config/test/storage/storage.test.ts index 92cc12225e8..7a865107791 100644 --- a/packages/remote-config/test/storage/storage.test.ts +++ b/packages/remote-config/test/storage/storage.test.ts @@ -117,4 +117,58 @@ describe('Storage', () => { expect(actualMetadata).to.be.undefined; }); + + it('sets and gets custom signals', async () => { + const customSignals = { key: 'value', key1: 'value1', key2: 1 }; + const customSignalsInStorage = { + key: 'value', + key1: 'value1', + key2: '1' + }; + + await storage.setCustomSignals(customSignals); + + const storedCustomSignals = await storage.getCustomSignals(); + + expect(storedCustomSignals).to.deep.eq(customSignalsInStorage); + }); + + it('upserts custom signals when key is present in storage', async () => { + const customSignals = { key: 'value', key1: 'value1' }; + const updatedSignals = { key: 'value', key1: 'value2' }; + + await storage.setCustomSignals(customSignals); + + await storage.setCustomSignals({ key1: 'value2' }); + + const storedCustomSignals = await storage.getCustomSignals(); + + expect(storedCustomSignals).to.deep.eq(updatedSignals); + }); + + it('deletes custom signal when value supplied is null', async () => { + const customSignals = { key: 'value', key1: 'value1' }; + const updatedSignals = { key: 'value' }; + + await storage.setCustomSignals(customSignals); + + await storage.setCustomSignals({ key1: null }); + + const storedCustomSignals = await storage.getCustomSignals(); + + expect(storedCustomSignals).to.deep.eq(updatedSignals); + }); + + it('throws an error when supplied with excess custom signals', async () => { + const customSignals: { [key: string]: string } = {}; + for (let i = 0; i < 101; i++) { + customSignals[`key${i}`] = `value${i}`; + } + + await expect( + storage.setCustomSignals(customSignals) + ).to.eventually.be.rejectedWith( + 'Remote Config: Setting more than 100 custom signals is not supported.' + ); + }); }); diff --git a/packages/remote-config/test/storage/storage_cache.test.ts b/packages/remote-config/test/storage/storage_cache.test.ts index e7cfb0ef0da..8d11cfac46a 100644 --- a/packages/remote-config/test/storage/storage_cache.test.ts +++ b/packages/remote-config/test/storage/storage_cache.test.ts @@ -37,6 +37,7 @@ describe('StorageCache', () => { const status = 'success'; const lastSuccessfulFetchTimestampMillis = 123; const activeConfig = { key: 'value' }; + const customSignals = { 'key': 'value' }; storage.getLastFetchStatus = sinon .stub() @@ -47,12 +48,16 @@ describe('StorageCache', () => { storage.getActiveConfig = sinon .stub() .returns(Promise.resolve(activeConfig)); + storage.getCustomSignals = sinon + .stub() + .returns(Promise.resolve(customSignals)); await storageCache.loadFromStorage(); expect(storage.getLastFetchStatus).to.have.been.called; expect(storage.getLastSuccessfulFetchTimestampMillis).to.have.been.called; expect(storage.getActiveConfig).to.have.been.called; + expect(storage.getCustomSignals).to.have.been.called; expect(storageCache.getLastFetchStatus()).to.eq(status); expect(storageCache.getLastSuccessfulFetchTimestampMillis()).to.deep.eq( @@ -81,4 +86,26 @@ describe('StorageCache', () => { expect(storage.setActiveConfig).to.have.been.calledWith(activeConfig); }); }); + + describe('setCustomSignals', () => { + const customSignals = { key: 'value' }; + + beforeEach(() => { + storage.setCustomSignals = sinon + .stub() + .returns(Promise.resolve(customSignals)); + }); + + it('writes to memory cache', async () => { + await storageCache.setCustomSignals(customSignals); + + expect(storageCache.getCustomSignals()).to.deep.eq(customSignals); + }); + + it('writes to persistent storage', async () => { + await storageCache.setCustomSignals(customSignals); + + expect(storage.setCustomSignals).to.have.been.calledWith(customSignals); + }); + }); }); diff --git a/packages/util/index.node.ts b/packages/util/index.node.ts index 9c3b54b1c86..d839460713c 100644 --- a/packages/util/index.node.ts +++ b/packages/util/index.node.ts @@ -38,7 +38,6 @@ export * from './src/sha1'; export * from './src/subscribe'; export * from './src/validation'; export * from './src/utf8'; -export * from './src/uuid'; export * from './src/exponential_backoff'; export * from './src/formatters'; export * from './src/compat'; diff --git a/packages/util/index.ts b/packages/util/index.ts index 38b944cd9b5..51c27c31099 100644 --- a/packages/util/index.ts +++ b/packages/util/index.ts @@ -33,7 +33,6 @@ export * from './src/sha1'; export * from './src/subscribe'; export * from './src/validation'; export * from './src/utf8'; -export * from './src/uuid'; export * from './src/exponential_backoff'; export * from './src/formatters'; export * from './src/compat'; diff --git a/packages/util/src/uuid.ts b/packages/util/src/uuid.ts deleted file mode 100644 index d931d4644e2..00000000000 --- a/packages/util/src/uuid.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @license - * Copyright 2022 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. - */ - -/** - * Copied from https://stackoverflow.com/a/2117523 - * Generates a new uuid. - * @public - */ -export const uuidv4 = function (): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { - const r = (Math.random() * 16) | 0, - v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -}; diff --git a/scripts/release/poll-npm-publish.js b/scripts/release/poll-npm-publish.js new file mode 100644 index 00000000000..c9d454eb683 --- /dev/null +++ b/scripts/release/poll-npm-publish.js @@ -0,0 +1,68 @@ +/** + * @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. + */ + +const { exec } = require('child_process'); + +const MAX_ATTEMPTS = 15; +const RETRY_DELAY_SECONDS = 60; + +async function pollNpmPublish() { + const version = process.env.VERSION; + + if (!version) { + console.log(`Couldn't find env var VERSION.`); + return; + } + + const getNpmPublishedVersion = () => + new Promise((resolve, reject) => { + exec(`npm view firebase@${version} version`, (error, stdout) => { + if (error) { + reject(error); + } + const version = stdout.trim(); + if (!version.match(/^\d+(\.[-\d\w]+)+$/)) { + reject( + new Error( + `npm view did not return a valid tag. Received: ${version}` + ) + ); + } + resolve(version); + }); + }); + for (let i = 0; i < MAX_ATTEMPTS; i++) { + const latestPublishedVersion = await getNpmPublishedVersion(); + if (latestPublishedVersion === process.env.VERSION) { + console.log(`Found firebase@${version} in the npm registry.`); + return; + } + console.log(`Didn't find firebase@${version} in the npm registry.`); + if (i < MAX_ATTEMPTS - 1) { + console.log(`Trying again in ${RETRY_DELAY_SECONDS} seconds.`); + await new Promise(resolve => + setTimeout(resolve, RETRY_DELAY_SECONDS * 1000) + ); + } + } + console.log( + `Was not able to find firebase@${version} on npm. Ending process.` + ); + process.exit(1); +} + +pollNpmPublish();