From 88be9bb5437ada5a0e56328598233be1a3d22889 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 29 Aug 2025 15:36:30 -0700 Subject: [PATCH 01/31] feat(docs): add initial developer documentation This commit introduces a new developer documentation hub under the `devdocs` directory. The goal of this new documentation is to provide a comprehensive resource for developers working on the Firestore JS SDK. The new documentation includes: - **Overview:** A high-level overview of the SDK, its goals, and architecture. - **Prerequisites:** A guide for new contributors, outlining the necessary skills and knowledge. - **Architecture:** A detailed explanation of the SDK's architecture, core components, and data flow. - **Code Layout:** A document that explains the structure of the codebase. - **Build & Testing:** Initial documents for the build and testing processes. --- packages/firestore/GEMINI.md | 5 ++ packages/firestore/devdocs/architecture.md | 87 ++++++++++++++++++++ packages/firestore/devdocs/architecture.png | Bin 0 -> 121882 bytes packages/firestore/devdocs/build.md | 3 + packages/firestore/devdocs/code-layout.md | 23 ++++++ packages/firestore/devdocs/overview.md | 45 ++++++++++ packages/firestore/devdocs/prerequisites.md | 31 +++++++ packages/firestore/devdocs/testing.md | 15 ++++ 8 files changed, 209 insertions(+) create mode 100644 packages/firestore/GEMINI.md create mode 100644 packages/firestore/devdocs/architecture.md create mode 100644 packages/firestore/devdocs/architecture.png create mode 100644 packages/firestore/devdocs/build.md create mode 100644 packages/firestore/devdocs/code-layout.md create mode 100644 packages/firestore/devdocs/overview.md create mode 100644 packages/firestore/devdocs/prerequisites.md create mode 100644 packages/firestore/devdocs/testing.md diff --git a/packages/firestore/GEMINI.md b/packages/firestore/GEMINI.md new file mode 100644 index 0000000000..e4ce4c2123 --- /dev/null +++ b/packages/firestore/GEMINI.md @@ -0,0 +1,5 @@ +# Firestore JavaScript SDK +This project is the official JavaScript SDK for the [Google Cloud Firestore](https://firebase.google.com/docs/firestore) database. + +You are an expert in @devdocs/prerequisites.md +@devdocs/overview.md \ No newline at end of file diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md new file mode 100644 index 0000000000..daa18a457c --- /dev/null +++ b/packages/firestore/devdocs/architecture.md @@ -0,0 +1,87 @@ +# SDK Architecture + +This document provides a detailed explanation of the Firestore JavaScript SDK's architecture, its core components, and the flow of data through the system. + +## Core Components + +The SDK is composed of several key components that work together to provide the full range of Firestore features. + +![Architecture Diagram](./architecture.png) + +* **API Layer**: The public-facing API surface that developers use to interact with the SDK. This layer is responsible for translating the public API calls into the internal data models and passing them to the appropriate core components. +* **Core**: + * **Event Manager**: Acts as a central hub for all eventing in the SDK. It is responsible for routing events between the API Layer and Sync Engine. It manages query listeners and is responsible for raising snapshot events, as well as handling connectivity changes and some query failures. + * **Sync Engine**: The central controller of the SDK. It acts as the glue between the Event Manager, Local Store, and Remote Store. Its responsibilities include: + * Coordinating client requests and remote events. + * Managing a view for each query, which represents the unified view between the local and remote data stores. + * Notifying the Remote Store when the Local Store has new mutations that need to be sent to the backend. +* **Local Store**: A container for the components that manage persisted and in-memory data. + * **Remote Table**: A cache of the most recent version of documents as known by the Firestore backend. + * **Mutation Queue**: A queue of all the user-initiated writes (set, update, delete) that have not yet been acknowledged by the Firestore backend. + * **Local View**: A cache that represents the user's current view of the data, combining the Remote Table with the Mutation Queue. +* **Remote Store**: The component responsible for all network communication with the Firestore backend. It manages the gRPC streams for reading and writing data, and it abstracts away the complexities of the network protocol from the rest of the SDK. +* **Persistence Layer**: The underlying storage mechanism used by the Local Store to persist data on the client. In the browser, this is implemented using IndexedDB. + +The architecture and systems within the SDK map closely to the directory structure, which helps developers navigate the codebase. Here is a mapping of the core components to their corresponding directories. + +* `src/`: + * `api/`: Implements the **API Layer** for the main SDK. + * `lite-api/`: Implements the **API Layer** for the lite SDK. + * `core/`: Implements the **Sync Engine** and **Event Manager**. + * `local/`: Implements the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, and the **Persistence Layer**. + * `remote/`: Implements the **Remote Store**, handling all network communication. + +For a more detailed explanation of the contents of each directory, see the [Code Layout](./code-layout.md) documentation. + +## Overview of features + +At a high level, all interactions with Firestore can be categorized as either reading or writing data. The SDK provides different mechanisms for these operations, each with distinct guarantees and performance characteristics. There is also a special case of writing data called tansactions detailed below. + + +### Read Operations + +There are two fundamental ways to read data from Firestore: + +* **One-Time Reads**: This is for fetching a snapshot of data at a specific moment. It's a simple request-response model. You ask for a document or the results of a query, and the server sends back the data as it exists at that instant. + +* **Real-Time Listeners**: This allows you to subscribe to a document or a query. The server first sends you the initial data and then continues to push updates to your client in real time as the data changes. This is the foundation of Firestore's real-time capabilities. + +When a query is executed, the SDK immediately returns data from the local cache, which includes any pending optimistic writes from the **Mutation Queue**. This provides a fast, responsive experience. At the same time, the SDK sends the query to the Firestore backend to fetch the latest version of the documents. When the fresh documents arrive from the backend, the SDK takes these server-authoritative documents and re-applies any pending mutations from the local queue on top of them. It then re-runs the original query against this newly merged data. If the documents still match the query's criteria, they are delivered to the query listener again. This is a common occurrence and means a listener could see an event for the same document twice: first with the cached, optimistic data, and a second time after the backend data is reconciled. + +### Write Operations + +All data modifications—creates, updates, and deletes—are treated as "writes." The SDK is designed to make writes atomic and resilient. There are two fundamental ways to write data to Firestore: + +* **One-Time Writes**: When a user performs a write (create, update, or delete), the operation is not sent directly to the backend. Instead, it's treated as a "mutation" and added to the local **Mutation Queue**. The SDK "optimistically" assumes the write will succeed on the backend and immediately reflects the change in the local view of the data, making the change visible to local queries. The SDK then works to synchronize this queue with the backend. This design is crucial for supporting offline functionality, as pending writes can be retried automatically when network connectivity is restored. + +* **Transactions**: For grouping multiple write operations into a single atomic unit, the SDK provides `runTransaction`. Unlike standard writes, transactions do not use the optimistic, offline-capable write pipeline. Instead, they are sent directly to the backend, which requires an active internet connection. This ensures atomicity but means transactions do not benefit from the offline capabilities of the standard write pipeline. + + +# Data Flow + +Here's a step-by-step walkthrough of how data flows through the SDK for a write operation, referencing the core components. + +## Write Data Flow + +1. **API Layer**: A user initiates a write operation (e.g., `setDoc`, `updateDoc`, `deleteDoc`). +2. **Sync Engine**: The call is routed to the Sync Engine, which wraps the operation in a "mutation". +3. **Mutation Queue (in Local Store)**: The Sync Engine adds this mutation to the Mutation Queue. The queue is persisted to the **Persistence Layer** (IndexedDB). At this point, the SDK "optimistically" considers the write successful locally. +4. **Local View (in Local Store)**: The change is immediately reflected in the Local View, making it available to any active listeners without waiting for backend confirmation. +5. **Remote Store**: The Sync Engine notifies the Remote Store that there are pending mutations. +6. **Backend**: The Remote Store sends the mutations from the queue to the Firestore backend. +7. **Acknowledgement**: The backend acknowledges the write. +8. **Mutation Queue (in Local Store)**: The Remote Store informs the Sync Engine, which then removes the acknowledged mutation from the Mutation Queue. + +## Read Data Flow (with a Real-Time Listener) + +1. **API Layer**: A user attaches a listener to a query (e.g., `onSnapshot`). +2. **Event Manager**: The Event Manager creates a listener and passes it to the Sync Engine. +3. **Sync Engine**: The Sync Engine creates a "view" for the query. +4. **Local View (in Local Store)**: The Sync Engine asks the Local Store for the current documents matching the query. This includes any optimistic local changes from the **Mutation Queue**. +5. **API Layer**: The initial data from the Local View is sent back to the user's `onSnapshot` callback. This provides a fast, initial result. +6. **Remote Store**: Simultaneously, the Sync Engine instructs the Remote Store to listen to the query on the Firestore backend. +7. **Backend**: The backend returns the initial matching documents for the query. +8. **Remote Table (in Local Store)**: The Remote Store receives the documents and saves them to the Remote Table in the Local Store, overwriting any previously cached versions of those documents. +9. **Sync Engine**: The Sync Engine is notified of the updated documents. It re-calculates the query view by combining the new data from the Remote Table with any applicable pending mutations from the **Mutation Queue**. +10. **API Layer**: If the query results have changed after this reconciliation, the new results are sent to the user's `onSnapshot` callback. This is why a listener may fire twice initially. +11. **Real-time Updates**: From now on, any changes on the backend that affect the query are pushed to the Remote Store, which updates the Remote Table, triggering the Sync Engine to re-calculate the view and notify the listener. \ No newline at end of file diff --git a/packages/firestore/devdocs/architecture.png b/packages/firestore/devdocs/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..ec1fc04010b8e36aca764f5ef3e0618f50a1aa64 GIT binary patch literal 121882 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ࠢ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 L=S+^8)y@9`wN9R< literal 0 HcmV?d00001 diff --git a/packages/firestore/devdocs/build.md b/packages/firestore/devdocs/build.md new file mode 100644 index 0000000000..55b5e7b395 --- /dev/null +++ b/packages/firestore/devdocs/build.md @@ -0,0 +1,3 @@ +# Build Process + +This document provides a detailed explanation of the Firestore JavaScript SDK build process for the main and lite packages. diff --git a/packages/firestore/devdocs/code-layout.md b/packages/firestore/devdocs/code-layout.md new file mode 100644 index 0000000000..619cc329e7 --- /dev/null +++ b/packages/firestore/devdocs/code-layout.md @@ -0,0 +1,23 @@ +# SDK Code Layout + +This document explains the code layout in this repository. It is closely related to the [architecture](./architecture.md). + +* `src/`: Contains the source code for the main `@firebase/firestore` package. + * `api/`: Implements the **API Layer** for the main SDK. + * `lite-api/`: Contains the entry point of for the lite SDK. + * `core/`: Contains logic for the **Sync Engine** and **Event Manager**. + * `local/`: Contains the logic the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, and the **Persistence Layer**. + * `remote/`: Contains the logic for the **Remote Store**, handling all network communication. + * `model/`: Defines the internal data models used throughout the SDK, such as `Document`, `DocumentKey`, and `Mutation`. These models are used to represent Firestore data and operations in a structured way. + * `platform/`: Contains platform-specific code to abstract away the differences between the Node.js and browser environments. This includes things like networking, storage, and timers. This allows the core logic of the SDK to be platform-agnostic. + * `protos/`: Contains the Protocol Buffer (`.proto`) definitions that describe the gRPC API surface of the Firestore backend. These files are used to generate the client-side networking code. +* `lite/`: Defines the entrypoint code for the `@firebase/firestore/lite` package. +* `test/`: Contains all unit and integration tests for the SDK. The tests are organized by component and feature, and they are essential for ensuring the quality and correctness of the code. +* `scripts/`: Contains a collection of build and maintenance scripts used for tasks such as bundling the code, running tests, and generating documentation. + +TODO: Add more detailed information as appropriate on each folder + +TODO: Mention critical entry points + - `package.json` for packages and common commands. Go to [build.md](./build.md) for details + - rollup configs for main and lite sdks. Go to [build.md](./build.md) for details + - tests entry points. Go to [testing.md](./testing.md) for details \ No newline at end of file diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md new file mode 100644 index 0000000000..305aa77475 --- /dev/null +++ b/packages/firestore/devdocs/overview.md @@ -0,0 +1,45 @@ +# Firestore JavaScript SDK Overview + +This document is the starting point for navigating the Firestore JavaScript SDK codebase documentation. It provides a high-level overview of the SDK, how it is built, tested, and the developer workflow. + +All contributors are expected to be familiar with the [prerequisites](./prerequisites.md) before working in this codebase. + +## Project Goals + +The Firestore JavaScript SDK is one of the official client-side library for interacting with [Google Cloud Firestore](https://firebase.google.com/docs/firestore). It is designed to be used in a variety of JavaScript environments, including web browsers (primary and common) and Node.js (secondary and rare). It is important to distinguish this SDK from the [Google Cloud Firestore server-side SDK for Node.js](https://github.com/googleapis/nodejs-firestore). While this SDK can run in Node.js, it is primarily designed for client-side use. The server-side SDK is intended for trusted environments and offers different capabilities. However, the two SDKs are designed to harmonize where helpful (e.g. data models) to facilitate easier full-stack application development. + +The primary goals of this SDK are: + +* Provide a simple and intuitive API for reading and writing data to Firestore. +* Support real-time data synchronization with streaming queries. +* Enable offline data access and query caching. +* Offer a lightweight version for applications that do not require advanced features. +* Maintain API and architectural symmetry with the [Firestore Android SDK](https://github.com/firebase/firebase-android-sdk) and [Firestore iOS SDK](https://github.com/firebase/firebase-ios-sdk). This consistency simplifies maintenance and makes it easier to port features between platforms. The public API is intentionally consistent across platforms, even if it means being less idiomatic, to allow developers to more easily port their application code. + +## Artifacts + +The Firestore JavaScript SDK is divided into two main packages: + +* `@firebase/firestore`: The main, full-featured SDK that provides streaming and offline support. +* `@firebase/firestore/lite`: A much lighter-weight (AKA "lite") version of the SDK for applications that do not require streaming or offline support. + +For a detailed explanation of the architecture, components, and data flow, please see the [Architecture documentation](./architecture.md). Related, for a deailed overview of the source code layout, please see [Code layout](./code-layout.md). + + +## Build + +TODO: Add critical information about the build process including optimizations for code size, etc. + +For information on how the artifacts are built, please see the [Build documentation](./build.md) file. + +## Testing + +TODO: Add critical information about the tests harness, organization, spec tests, etc. + +For information on how the tests are setup and organized [Testing documentation](./testing.md) file. + +## Developer Workflow + +TODO: Add list of common commands here. + +For information on the developer workflow, including how to build, test, and format the code, please see the [CONTRIBUTING.md](../CONTRIBUTING.md) file. diff --git a/packages/firestore/devdocs/prerequisites.md b/packages/firestore/devdocs/prerequisites.md new file mode 100644 index 0000000000..dabe19d7b9 --- /dev/null +++ b/packages/firestore/devdocs/prerequisites.md @@ -0,0 +1,31 @@ +# Firestore JavaScript SDK Maintainer's Guide + +This document outlines the prerequisite knowledge for new maintainers of the Firestore JavaScript SDK. + +## Prerequisite Knowledge + +Before contributing to this codebase, you should have a strong understanding of the following technologies and concepts: + +### Core Technologies + +* **TypeScript:** The entire codebase is written in TypeScript. A deep understanding of TypeScript, including its type system, generics, and modules, is essential. +* **JavaScript (ES6+):** As a JavaScript SDK, a strong grasp of modern JavaScript features is required. +* **Node.js:** The SDK is isomorphic and runs in the Node.js environment. Familiarity with Node.js concepts, such as its module system and event loop, is important. +* **Browser Runtime Environment:** The SDK is also used in web browsers. A good understanding of the different browser execution contexts (e.g. main window, web/service workers) and subsystems (e.g. persistence like IndexedDB and Local Storage, networking) is necessary. + +### Build and Test Tooling + +* **Yarn:** We use Yarn for package management. You should be familiar with basic Yarn commands. +* **Rollup.js:** Our build process uses Rollup.js to bundle the code. Understanding Rollup's configuration and plugin system will be helpful. +* **Karma, Mocha, and Chai:** These are our testing frameworks. You should be comfortable writing and running tests using this stack. + + + +### Domain Knowledge + +* **[Google Cloud Firestore](https://firebase.google.com/docs/firestore):** A deep understanding of Firestore's data model (documents, collections, subcollections), query language, and security rules is fundamental. +* **Databases:** A general understanding of databases, including key-value stores and relational databases, is helpful for understanding Firestore's design and trade-offs. +* **Modern Web Application Architecture:** Familiarity with modern web application architecture and also server-side rendering (SSR), is beneficial for understanding how the SDK is used in practice. +* **[Firebase](https://firebase.google.com/docs):** Familiarity with the Firebase platform is required, especially Firebase Auth and Firebase Functions. +* **Protocol Buffers / gRPC:** The main SDK uses Protocol Buffers over gRPC to communicate with the Firestore backend. A basic understanding of these technologies is helpful. +* **Firestore REST API:** The lite SDK uses the Firestore REST API. Familiarity with the REST API is useful when working on the lite version of the SDK. diff --git a/packages/firestore/devdocs/testing.md b/packages/firestore/devdocs/testing.md new file mode 100644 index 0000000000..11f00ebca8 --- /dev/null +++ b/packages/firestore/devdocs/testing.md @@ -0,0 +1,15 @@ +# Build Process + +This document provides a detailed explanation of the Firestore JavaScript SDK testing strategy, tech stack, and patterns and practices. + +# Tech Stack +- karma, mocha, chai + +# Strategy +- Firebase emulator for local development +- Integration testing with the backend + +# Patterns and Practices + + +# Spec Tests \ No newline at end of file From 2e32df3d03a98de5fa040676a6057092d6612a73 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Tue, 4 Nov 2025 12:21:58 -0500 Subject: [PATCH 02/31] Refining overview --- packages/firestore/devdocs/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index 305aa77475..6f4f52a437 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -1,6 +1,6 @@ # Firestore JavaScript SDK Overview -This document is the starting point for navigating the Firestore JavaScript SDK codebase documentation. It provides a high-level overview of the SDK, how it is built, tested, and the developer workflow. +This document is the starting point for navigating the Firestore JavaScript SDK codebase and its documentation. It provides a high-level overview of the SDK, its architecture, how it is built, tested, and the developer workflow. All contributors are expected to be familiar with the [prerequisites](./prerequisites.md) before working in this codebase. From 3210cfa60390f162338ed855fdee65de01f2d8d3 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Tue, 4 Nov 2025 20:22:32 -0500 Subject: [PATCH 03/31] docs: Clarify Overlays in architecture and code layout documentation Updated to provide a detailed explanation of the 'Overlays' component within the Local Store, describing its purpose as a performance-optimizing cache for pending mutations. Also updated to consistently list 'Overlays' as a component of the directory, aligning with the architectural overview. --- packages/firestore/devdocs/architecture.md | 7 ++++--- packages/firestore/devdocs/code-layout.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index daa18a457c..66d960c694 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -19,6 +19,7 @@ The SDK is composed of several key components that work together to provide the * **Remote Table**: A cache of the most recent version of documents as known by the Firestore backend. * **Mutation Queue**: A queue of all the user-initiated writes (set, update, delete) that have not yet been acknowledged by the Firestore backend. * **Local View**: A cache that represents the user's current view of the data, combining the Remote Table with the Mutation Queue. + * **Overlays**: A performance-optimizing cache that stores the calculated effect of pending mutations from the Mutation Queue on documents. Instead of re-applying mutations every time a document is read, the SDK computes this "overlay" once and caches it, allowing the Local View to be constructed more efficiently. * **Remote Store**: The component responsible for all network communication with the Firestore backend. It manages the gRPC streams for reading and writing data, and it abstracts away the complexities of the network protocol from the rest of the SDK. * **Persistence Layer**: The underlying storage mechanism used by the Local Store to persist data on the client. In the browser, this is implemented using IndexedDB. @@ -28,7 +29,7 @@ The architecture and systems within the SDK map closely to the directory structu * `api/`: Implements the **API Layer** for the main SDK. * `lite-api/`: Implements the **API Layer** for the lite SDK. * `core/`: Implements the **Sync Engine** and **Event Manager**. - * `local/`: Implements the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, and the **Persistence Layer**. + * `local/`: Implements the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, **Overlays** and the **Persistence Layer**. * `remote/`: Implements the **Remote Store**, handling all network communication. For a more detailed explanation of the contents of each directory, see the [Code Layout](./code-layout.md) documentation. @@ -66,7 +67,7 @@ Here's a step-by-step walkthrough of how data flows through the SDK for a write 1. **API Layer**: A user initiates a write operation (e.g., `setDoc`, `updateDoc`, `deleteDoc`). 2. **Sync Engine**: The call is routed to the Sync Engine, which wraps the operation in a "mutation". 3. **Mutation Queue (in Local Store)**: The Sync Engine adds this mutation to the Mutation Queue. The queue is persisted to the **Persistence Layer** (IndexedDB). At this point, the SDK "optimistically" considers the write successful locally. -4. **Local View (in Local Store)**: The change is immediately reflected in the Local View, making it available to any active listeners without waiting for backend confirmation. +4. **Local View (in Local Store)**: The change is reflected in the Local View. This is done by creating or updating a cached **Overlay** for the affected document, making the change efficiently available to any active listeners without waiting for backend confirmation. 5. **Remote Store**: The Sync Engine notifies the Remote Store that there are pending mutations. 6. **Backend**: The Remote Store sends the mutations from the queue to the Firestore backend. 7. **Acknowledgement**: The backend acknowledges the write. @@ -77,7 +78,7 @@ Here's a step-by-step walkthrough of how data flows through the SDK for a write 1. **API Layer**: A user attaches a listener to a query (e.g., `onSnapshot`). 2. **Event Manager**: The Event Manager creates a listener and passes it to the Sync Engine. 3. **Sync Engine**: The Sync Engine creates a "view" for the query. -4. **Local View (in Local Store)**: The Sync Engine asks the Local Store for the current documents matching the query. This includes any optimistic local changes from the **Mutation Queue**. +4. **Local View (in Local Store)**: The Sync Engine asks the Local Store for the current documents matching the query. The Local Store provides these by applying cached **Overlays** on top of the documents to reflect optimistic local changes from the **Mutation Queue**. 5. **API Layer**: The initial data from the Local View is sent back to the user's `onSnapshot` callback. This provides a fast, initial result. 6. **Remote Store**: Simultaneously, the Sync Engine instructs the Remote Store to listen to the query on the Firestore backend. 7. **Backend**: The backend returns the initial matching documents for the query. diff --git a/packages/firestore/devdocs/code-layout.md b/packages/firestore/devdocs/code-layout.md index 619cc329e7..0bdd6e4a38 100644 --- a/packages/firestore/devdocs/code-layout.md +++ b/packages/firestore/devdocs/code-layout.md @@ -6,7 +6,7 @@ This document explains the code layout in this repository. It is closely related * `api/`: Implements the **API Layer** for the main SDK. * `lite-api/`: Contains the entry point of for the lite SDK. * `core/`: Contains logic for the **Sync Engine** and **Event Manager**. - * `local/`: Contains the logic the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, and the **Persistence Layer**. + * `local/`: Contains the logic the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, **Overlays**, and the **Persistence Layer**. * `remote/`: Contains the logic for the **Remote Store**, handling all network communication. * `model/`: Defines the internal data models used throughout the SDK, such as `Document`, `DocumentKey`, and `Mutation`. These models are used to represent Firestore data and operations in a structured way. * `platform/`: Contains platform-specific code to abstract away the differences between the Node.js and browser environments. This includes things like networking, storage, and timers. This allows the core logic of the SDK to be platform-agnostic. From caa119648d3f50be0aaba4e70222d3e83c41c970 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Tue, 4 Nov 2025 21:52:53 -0500 Subject: [PATCH 04/31] gemini: saving custom docs gemini command --- packages/firestore/.gemini/commands/docs.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/firestore/.gemini/commands/docs.toml diff --git a/packages/firestore/.gemini/commands/docs.toml b/packages/firestore/.gemini/commands/docs.toml new file mode 100644 index 0000000000..1f271c711e --- /dev/null +++ b/packages/firestore/.gemini/commands/docs.toml @@ -0,0 +1,13 @@ +description = "Generate documentation for a high level concept in the codebase" +prompt = """ + +Your primary role is to help explain this codebase and update the existing documentation with useful information for future maintainers new to the codebase. + +1. **Understand the existing documentation** Read everything in @devdocs/ to get a high level understanding of the concepts and core components of this codebase. +2. **Perform archeology on the codebase** Read everything in {{args.file_or_folder}} and learn about {{args.concept}}. +3. **Verify your understanding of the concept** Stop and summarize what {{args.concept}} is. Ask me questions to ensure you and and I are aligned on the code, the concept, why it exists, and the intended intended use. Summarize what you learn. Ask me as many questions as necessary until you I tell you I'm ready to document what you know. +4. **Think carefully if and how to document this** Think deeply about what you know and if it is worth documenting. If so, think about the best way to document it. Documentation files, code comments, or both. Prefer high level documentation. +5. **Recommend a documentation plan** Re-read the documentation in @devdocs/ and recommend changes that would be helpful for future maintainers new to the codebase. Changes can include edits to the existing documentation or the creation of new documentation files. Edits to existing files must align to their purpose and level of detail. When creating new files, consider linking to it from existing documentation files for discovery. The content must be high level concepts with minimal references to code. Iterate with me until I'm satisfied with the proposed edits. + +Your final output is a recommendation to the documentation. +""" \ No newline at end of file From 5bea3dc6cd7db7c4b9fc3c696557dd85afe83228 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Tue, 4 Nov 2025 21:54:17 -0500 Subject: [PATCH 05/31] docs(firestore): add and update documentation for bundles Adds new documentation and updates existing documentation for Firestore data bundles. The new `bundles.md` file provides a deep dive into the concept of bundles, their primary use case for SSR hydration, and other benefits. The `architecture.md` file has been updated to include a high-level overview of bundles and their data flow, consistent with the rest of the document. --- packages/firestore/devdocs/architecture.md | 18 ++++++- packages/firestore/devdocs/bundles.md | 58 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 packages/firestore/devdocs/bundles.md diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index 66d960c694..d839a21798 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -57,6 +57,15 @@ All data modifications—creates, updates, and deletes—are treated as "writes. * **Transactions**: For grouping multiple write operations into a single atomic unit, the SDK provides `runTransaction`. Unlike standard writes, transactions do not use the optimistic, offline-capable write pipeline. Instead, they are sent directly to the backend, which requires an active internet connection. This ensures atomicity but means transactions do not benefit from the offline capabilities of the standard write pipeline. +### Data Bundles + +A Firestore data bundle is a serialized collection of documents and query results, created on a server using the Firebase Admin SDK. Bundles are used to efficiently deliver a pre-packaged set of data to the client, which can then be loaded directly into the SDK's local cache. This is useful for: + +* **Seeding initial data** for an application, allowing users to have a complete offline experience on their first use. +* **Distributing curated datasets** to clients in a single, efficient package. + +When a bundle is loaded, its contents are unpacked and stored in the local cache, making the data available for immediate querying without needing to connect to the Firestore backend. For more details, see the [Bundles documentation](./bundles.md). + # Data Flow @@ -85,4 +94,11 @@ Here's a step-by-step walkthrough of how data flows through the SDK for a write 8. **Remote Table (in Local Store)**: The Remote Store receives the documents and saves them to the Remote Table in the Local Store, overwriting any previously cached versions of those documents. 9. **Sync Engine**: The Sync Engine is notified of the updated documents. It re-calculates the query view by combining the new data from the Remote Table with any applicable pending mutations from the **Mutation Queue**. 10. **API Layer**: If the query results have changed after this reconciliation, the new results are sent to the user's `onSnapshot` callback. This is why a listener may fire twice initially. -11. **Real-time Updates**: From now on, any changes on the backend that affect the query are pushed to the Remote Store, which updates the Remote Table, triggering the Sync Engine to re-calculate the view and notify the listener. \ No newline at end of file +11. **Real-time Updates**: From now on, any changes on the backend that affect the query are pushed to the Remote Store, which updates the Remote Table, triggering the Sync Engine to re-calculate the view and notify the listener. + +## Bundle Loading Data Flow + +1. **API Layer**: The user initiates a bundle load via the public API. +2. **Sync Engine**: The Sync Engine receives the bundle and begins processing it. +3. **Local Store**: The Sync Engine unpacks the bundle and saves its contents (documents and named queries) into the **Local Store**. +4. **API Layer**: The user is notified of the progress and completion of the bundle loading operation via the task returned by the API. \ No newline at end of file diff --git a/packages/firestore/devdocs/bundles.md b/packages/firestore/devdocs/bundles.md new file mode 100644 index 0000000000..26412a98cc --- /dev/null +++ b/packages/firestore/devdocs/bundles.md @@ -0,0 +1,58 @@ +# Firestore Data Bundles + +This document provides a deep dive into the concept of Firestore data bundles, how they are processed, and how they are used within the SDK. + +## What is a Bundle? + +A Firestore data bundle is a serialized, read-only collection of documents and named query results. Bundles are created on a server using the Firebase Admin SDK and can be efficiently distributed to clients. + +While bundles can be used for several purposes, their primary design motivation is to optimize Server-Side Rendering (SSR) workflows. In an SSR setup, a server pre-renders a page with data from Firestore. This data can be packaged into a bundle and sent to the client along with the HTML. The client-side SDK can then load this bundle and "hydrate" a real-time query with the pre-existing data, avoiding the need to re-fetch the same documents from the backend. This results in a significant performance improvement and cost savings. + +## Primary Use Case: Server-Side Rendering (SSR) Hydration + +The main workflow for bundles is as follows: + +1. **Server-Side:** A server fetches data from Firestore to render a page. +2. **Bundling:** The server packages the fetched documents and the corresponding query into a bundle. +3. **Transmission:** The bundle is embedded in the HTML page sent to the client. +4. **Client-Side:** The client-side JavaScript calls `loadBundle()` to load the data from the bundle into the SDK's local cache. +5. **Hydration:** The client then attaches a real-time listener to the same query that was bundled. The SDK finds the query results in the local cache and immediately fires the listener with the initial data, avoiding a costly roundtrip to the backend. + +## Other Benefits and Use Cases + +Beyond SSR hydration, bundles offer several other advantages: + +* **Enhanced Offline Experience:** Bundles can be shipped with an application's initial assets, allowing users to have a more complete offline experience from the first launch, reducing the need to sync every document individually. +* **Efficient Data Distribution:** They provide an efficient way to deliver curated or static datasets to clients in a single package. For instance, an application could bundle a list of popular items or configuration data. + +## The Loading Process + +The process of loading a bundle into the Firestore SDK is initiated by the `loadBundle()` method. This method returns a `LoadBundleTask`, which allows the developer to track the progress of the loading operation. + +Here's a step-by-step walkthrough of what happens when `loadBundle()` is called: + +1. **`loadBundle()` called:** The developer calls `loadBundle()` with the bundle data (as a `ReadableStream` or `ArrayBuffer`). +2. **`LoadBundleTask` created:** A `LoadBundleTask` is created and returned to the developer. This task acts as a `Promise` and also provides progress updates. +3. **`BundleLoader` initiated:** Internally, a `BundleLoader` is created to process the bundle. +4. **Bundle processing:** The `BundleLoader` reads the bundle element by element. The bundle is a sequence of JSON objects, each representing a metadata element, a named query, or a document. +5. **Data caching:** As the `BundleLoader` processes the bundle, it saves the data to the local store: + * **Bundle Metadata:** The bundle's metadata is saved to the `BundleCache`. This is used to track which bundles have been loaded. + * **Named Queries:** Named queries are saved to the `BundleCache`. + * **Documents:** Documents are saved to the `RemoteDocumentCache`. +6. **Progress updates:** The `LoadBundleTask` is updated with progress information (e.g., bytes loaded, documents loaded) as the `BundleLoader` processes the bundle. +7. **Completion:** Once the `BundleLoader` has finished processing the bundle, the `LoadBundleTask` is marked as complete. + +## Error Handling + +Errors can occur during the bundle loading process for a variety of reasons, such as a malformed bundle or a storage issue. + +When an error occurs, the `LoadBundleTask` is put into an `'Error'` state. The error is surfaced to the developer in two ways: + +* **Promise rejection:** The `LoadBundleTask`'s promise is rejected with a `FirestoreError`. +* **`onProgress` observer:** If an `error` callback is provided to the `onProgress` method, it will be called with the `FirestoreError`. + +## Interacting with Bundled Data + +Once a bundle has been loaded, the data it contains is available for querying. If the bundle included named queries, you can use the `getNamedQuery()` method to retrieve a `Query` object, which can then be executed. + +When a named query is executed, the Firestore SDK will first attempt to fulfill the query from the local cache. If the results for the named query are available in the cache (because they were loaded from a bundle), they will be returned immediately, without a server roundtrip. From a64ecf61266efd5f8166a56d712de07bae4a31be Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Tue, 4 Nov 2025 21:58:59 -0500 Subject: [PATCH 06/31] fine tuning gemini command --- packages/firestore/.gemini/commands/docs.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/firestore/.gemini/commands/docs.toml b/packages/firestore/.gemini/commands/docs.toml index 1f271c711e..98a6b0aad5 100644 --- a/packages/firestore/.gemini/commands/docs.toml +++ b/packages/firestore/.gemini/commands/docs.toml @@ -3,9 +3,9 @@ prompt = """ Your primary role is to help explain this codebase and update the existing documentation with useful information for future maintainers new to the codebase. -1. **Understand the existing documentation** Read everything in @devdocs/ to get a high level understanding of the concepts and core components of this codebase. -2. **Perform archeology on the codebase** Read everything in {{args.file_or_folder}} and learn about {{args.concept}}. -3. **Verify your understanding of the concept** Stop and summarize what {{args.concept}} is. Ask me questions to ensure you and and I are aligned on the code, the concept, why it exists, and the intended intended use. Summarize what you learn. Ask me as many questions as necessary until you I tell you I'm ready to document what you know. +1. **Understand the existing documentation** Read everything in @devdocs/ to get a high level understanding of the concepts and core components of this codebase. Look for any relevant information on {{args.concept}}. +2. **Perform archeology on the codebase** Read everything in {{args.file_or_folder}} and learn more about {{args.concept}}. +3. **Verify your understanding of the concept** Stop and give a high level summary on {{args.concept}} is. Ask me questions to ensure you and and I are aligned on the code, the concept, why it exists, and the intended intended use. Create a high level summary of what you learned. Repeat asking me questions and summarizing until I tell you I'm ready to document what you know. 4. **Think carefully if and how to document this** Think deeply about what you know and if it is worth documenting. If so, think about the best way to document it. Documentation files, code comments, or both. Prefer high level documentation. 5. **Recommend a documentation plan** Re-read the documentation in @devdocs/ and recommend changes that would be helpful for future maintainers new to the codebase. Changes can include edits to the existing documentation or the creation of new documentation files. Edits to existing files must align to their purpose and level of detail. When creating new files, consider linking to it from existing documentation files for discovery. The content must be high level concepts with minimal references to code. Iterate with me until I'm satisfied with the proposed edits. From 2a6349cebde645effd87b4eee6582b31248d5313 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Tue, 4 Nov 2025 21:54:17 -0500 Subject: [PATCH 07/31] docs(firestore): add and update documentation for bundles The new `bundles.md` file provides a deep dive into the concept of bundles, their primary use case for SSR hydration, and other benefits. The `architecture.md` file has been updated to include a high-level overview of bundles and their data flow, consistent with the rest of the document. --- packages/firestore/devdocs/bundles.md | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/firestore/devdocs/bundles.md b/packages/firestore/devdocs/bundles.md index 26412a98cc..ae718049f8 100644 --- a/packages/firestore/devdocs/bundles.md +++ b/packages/firestore/devdocs/bundles.md @@ -27,29 +27,13 @@ Beyond SSR hydration, bundles offer several other advantages: ## The Loading Process -The process of loading a bundle into the Firestore SDK is initiated by the `loadBundle()` method. This method returns a `LoadBundleTask`, which allows the developer to track the progress of the loading operation. +When an application provides a bundle to the SDK, a loading process is initiated. The SDK reads the bundle, which is a stream of documents and queries, and saves each item into its local cache. This process is asynchronous, allowing the application to continue running while the data is being loaded in the background. -Here's a step-by-step walkthrough of what happens when `loadBundle()` is called: - -1. **`loadBundle()` called:** The developer calls `loadBundle()` with the bundle data (as a `ReadableStream` or `ArrayBuffer`). -2. **`LoadBundleTask` created:** A `LoadBundleTask` is created and returned to the developer. This task acts as a `Promise` and also provides progress updates. -3. **`BundleLoader` initiated:** Internally, a `BundleLoader` is created to process the bundle. -4. **Bundle processing:** The `BundleLoader` reads the bundle element by element. The bundle is a sequence of JSON objects, each representing a metadata element, a named query, or a document. -5. **Data caching:** As the `BundleLoader` processes the bundle, it saves the data to the local store: - * **Bundle Metadata:** The bundle's metadata is saved to the `BundleCache`. This is used to track which bundles have been loaded. - * **Named Queries:** Named queries are saved to the `BundleCache`. - * **Documents:** Documents are saved to the `RemoteDocumentCache`. -6. **Progress updates:** The `LoadBundleTask` is updated with progress information (e.g., bytes loaded, documents loaded) as the `BundleLoader` processes the bundle. -7. **Completion:** Once the `BundleLoader` has finished processing the bundle, the `LoadBundleTask` is marked as complete. +To give developers visibility into this process, the SDK provides progress updates, including the number of documents and bytes loaded so far. Once all the data has been successfully loaded into the cache, the SDK signals that the process is complete. ## Error Handling -Errors can occur during the bundle loading process for a variety of reasons, such as a malformed bundle or a storage issue. - -When an error occurs, the `LoadBundleTask` is put into an `'Error'` state. The error is surfaced to the developer in two ways: - -* **Promise rejection:** The `LoadBundleTask`'s promise is rejected with a `FirestoreError`. -* **`onProgress` observer:** If an `error` callback is provided to the `onProgress` method, it will be called with the `FirestoreError`. +The bundle loading process is designed to be robust. If an error is encountered at any point—for example, if the bundle data is malformed or there is an issue writing to the local cache—the entire operation is aborted. The SDK ensures that the application is notified of the failure, allowing developers to catch the error and implement appropriate fallback or recovery logic. ## Interacting with Bundled Data From b2c00db93220a97a82e9b1567aea4a1f5c28ec5a Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Wed, 5 Nov 2025 21:13:39 -0500 Subject: [PATCH 08/31] docs: refine bundles documentation and update gemini command Refines the introductory sentence of to reflect a high-level overview. Updates the command description and prompt for clarity and broader applicability. --- .../firestore/.gemini/commands/{docs.toml => docs/amend.toml} | 4 ++-- packages/firestore/devdocs/bundles.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename packages/firestore/.gemini/commands/{docs.toml => docs/amend.toml} (94%) diff --git a/packages/firestore/.gemini/commands/docs.toml b/packages/firestore/.gemini/commands/docs/amend.toml similarity index 94% rename from packages/firestore/.gemini/commands/docs.toml rename to packages/firestore/.gemini/commands/docs/amend.toml index 98a6b0aad5..13562a6044 100644 --- a/packages/firestore/.gemini/commands/docs.toml +++ b/packages/firestore/.gemini/commands/docs/amend.toml @@ -1,7 +1,7 @@ -description = "Generate documentation for a high level concept in the codebase" +description = "Updates code documentation with new content" prompt = """ -Your primary role is to help explain this codebase and update the existing documentation with useful information for future maintainers new to the codebase. +Your primary role is to help explain this codebase and update the existing documentation with useful information for future maintainers. 1. **Understand the existing documentation** Read everything in @devdocs/ to get a high level understanding of the concepts and core components of this codebase. Look for any relevant information on {{args.concept}}. 2. **Perform archeology on the codebase** Read everything in {{args.file_or_folder}} and learn more about {{args.concept}}. diff --git a/packages/firestore/devdocs/bundles.md b/packages/firestore/devdocs/bundles.md index ae718049f8..4e4256df32 100644 --- a/packages/firestore/devdocs/bundles.md +++ b/packages/firestore/devdocs/bundles.md @@ -1,6 +1,6 @@ # Firestore Data Bundles -This document provides a deep dive into the concept of Firestore data bundles, how they are processed, and how they are used within the SDK. +This document provides an overview of Firestore data bundles, how they are processed, and how they are used within the SDK. ## What is a Bundle? From 94f795560c1d3a01ec6a7001dc354c59b0a09543 Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:52:40 -0500 Subject: [PATCH 09/31] Simple edits (#9368) --- packages/firestore/devdocs/architecture.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index d839a21798..b5b7397c06 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -12,9 +12,12 @@ The SDK is composed of several key components that work together to provide the * **Core**: * **Event Manager**: Acts as a central hub for all eventing in the SDK. It is responsible for routing events between the API Layer and Sync Engine. It manages query listeners and is responsible for raising snapshot events, as well as handling connectivity changes and some query failures. * **Sync Engine**: The central controller of the SDK. It acts as the glue between the Event Manager, Local Store, and Remote Store. Its responsibilities include: - * Coordinating client requests and remote events. - * Managing a view for each query, which represents the unified view between the local and remote data stores. + * Coordinating and translating client requests and remote events from the backend. + * Initiating responses to user code from both remote events (backend updates) and local events (e.g. garbage collection). + * Managing a "view" for each query, which represents the unified view between the local and remote data stores. + * Deciding whether a document is in a "limbo" state (e.g. its state is unknown) and needs to be fetched from the backend. * Notifying the Remote Store when the Local Store has new mutations that need to be sent to the backend. + * For web clients, synchronizing query and mutation states across multiple tabs. * **Local Store**: A container for the components that manage persisted and in-memory data. * **Remote Table**: A cache of the most recent version of documents as known by the Firestore backend. * **Mutation Queue**: A queue of all the user-initiated writes (set, update, delete) that have not yet been acknowledged by the Firestore backend. @@ -61,6 +64,7 @@ All data modifications—creates, updates, and deletes—are treated as "writes. A Firestore data bundle is a serialized collection of documents and query results, created on a server using the Firebase Admin SDK. Bundles are used to efficiently deliver a pre-packaged set of data to the client, which can then be loaded directly into the SDK's local cache. This is useful for: +* **Optimizing Server-Side Rendering (SSR)**: Bundles enable efficient hydration of client-side applications with data pre-fetched during server-side rendering, reducing initial load times and backend roundtrips. * **Seeding initial data** for an application, allowing users to have a complete offline experience on their first use. * **Distributing curated datasets** to clients in a single, efficient package. From 43d4e92df8d612f058f0dcb56eea826069b5458a Mon Sep 17 00:00:00 2001 From: Christina Holland Date: Thu, 28 Aug 2025 16:59:01 -0700 Subject: [PATCH 10/31] remove accidental factory export (#9232) --- .changeset/stupid-files-reflect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stupid-files-reflect.md diff --git a/.changeset/stupid-files-reflect.md b/.changeset/stupid-files-reflect.md new file mode 100644 index 0000000000..b164d4b631 --- /dev/null +++ b/.changeset/stupid-files-reflect.md @@ -0,0 +1,5 @@ +--- +'@firebase/ai': patch +--- + +Remove accidental `factory` export. From 6cc819e6e97c764091e6bcd01f8fc3c90900edac Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 28 Aug 2025 17:22:07 -0700 Subject: [PATCH 11/31] Version Packages (#9233) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/stupid-files-reflect.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/stupid-files-reflect.md diff --git a/.changeset/stupid-files-reflect.md b/.changeset/stupid-files-reflect.md deleted file mode 100644 index b164d4b631..0000000000 --- a/.changeset/stupid-files-reflect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@firebase/ai': patch ---- - -Remove accidental `factory` export. From 4546c2db2110577c8c72c6157849d6737b71632e Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Tue, 4 Nov 2025 21:52:53 -0500 Subject: [PATCH 12/31] gemini: saving custom docs gemini command --- packages/firestore/.gemini/commands/docs.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/firestore/.gemini/commands/docs.toml diff --git a/packages/firestore/.gemini/commands/docs.toml b/packages/firestore/.gemini/commands/docs.toml new file mode 100644 index 0000000000..1f271c711e --- /dev/null +++ b/packages/firestore/.gemini/commands/docs.toml @@ -0,0 +1,13 @@ +description = "Generate documentation for a high level concept in the codebase" +prompt = """ + +Your primary role is to help explain this codebase and update the existing documentation with useful information for future maintainers new to the codebase. + +1. **Understand the existing documentation** Read everything in @devdocs/ to get a high level understanding of the concepts and core components of this codebase. +2. **Perform archeology on the codebase** Read everything in {{args.file_or_folder}} and learn about {{args.concept}}. +3. **Verify your understanding of the concept** Stop and summarize what {{args.concept}} is. Ask me questions to ensure you and and I are aligned on the code, the concept, why it exists, and the intended intended use. Summarize what you learn. Ask me as many questions as necessary until you I tell you I'm ready to document what you know. +4. **Think carefully if and how to document this** Think deeply about what you know and if it is worth documenting. If so, think about the best way to document it. Documentation files, code comments, or both. Prefer high level documentation. +5. **Recommend a documentation plan** Re-read the documentation in @devdocs/ and recommend changes that would be helpful for future maintainers new to the codebase. Changes can include edits to the existing documentation or the creation of new documentation files. Edits to existing files must align to their purpose and level of detail. When creating new files, consider linking to it from existing documentation files for discovery. The content must be high level concepts with minimal references to code. Iterate with me until I'm satisfied with the proposed edits. + +Your final output is a recommendation to the documentation. +""" \ No newline at end of file From b6b66bd8072df6a294bc83bbe3ef3c4da09581be Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Tue, 4 Nov 2025 21:58:59 -0500 Subject: [PATCH 13/31] fine tuning gemini command --- packages/firestore/.gemini/commands/docs.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/firestore/.gemini/commands/docs.toml b/packages/firestore/.gemini/commands/docs.toml index 1f271c711e..98a6b0aad5 100644 --- a/packages/firestore/.gemini/commands/docs.toml +++ b/packages/firestore/.gemini/commands/docs.toml @@ -3,9 +3,9 @@ prompt = """ Your primary role is to help explain this codebase and update the existing documentation with useful information for future maintainers new to the codebase. -1. **Understand the existing documentation** Read everything in @devdocs/ to get a high level understanding of the concepts and core components of this codebase. -2. **Perform archeology on the codebase** Read everything in {{args.file_or_folder}} and learn about {{args.concept}}. -3. **Verify your understanding of the concept** Stop and summarize what {{args.concept}} is. Ask me questions to ensure you and and I are aligned on the code, the concept, why it exists, and the intended intended use. Summarize what you learn. Ask me as many questions as necessary until you I tell you I'm ready to document what you know. +1. **Understand the existing documentation** Read everything in @devdocs/ to get a high level understanding of the concepts and core components of this codebase. Look for any relevant information on {{args.concept}}. +2. **Perform archeology on the codebase** Read everything in {{args.file_or_folder}} and learn more about {{args.concept}}. +3. **Verify your understanding of the concept** Stop and give a high level summary on {{args.concept}} is. Ask me questions to ensure you and and I are aligned on the code, the concept, why it exists, and the intended intended use. Create a high level summary of what you learned. Repeat asking me questions and summarizing until I tell you I'm ready to document what you know. 4. **Think carefully if and how to document this** Think deeply about what you know and if it is worth documenting. If so, think about the best way to document it. Documentation files, code comments, or both. Prefer high level documentation. 5. **Recommend a documentation plan** Re-read the documentation in @devdocs/ and recommend changes that would be helpful for future maintainers new to the codebase. Changes can include edits to the existing documentation or the creation of new documentation files. Edits to existing files must align to their purpose and level of detail. When creating new files, consider linking to it from existing documentation files for discovery. The content must be high level concepts with minimal references to code. Iterate with me until I'm satisfied with the proposed edits. From 62b873430ee0082ada541fe3e9b7486161b3b9af Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Wed, 5 Nov 2025 21:13:39 -0500 Subject: [PATCH 14/31] docs: refine bundles documentation and update gemini command Refines the introductory sentence of to reflect a high-level overview. Updates the command description and prompt for clarity and broader applicability. --- packages/firestore/.gemini/commands/docs.toml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 packages/firestore/.gemini/commands/docs.toml diff --git a/packages/firestore/.gemini/commands/docs.toml b/packages/firestore/.gemini/commands/docs.toml deleted file mode 100644 index 98a6b0aad5..0000000000 --- a/packages/firestore/.gemini/commands/docs.toml +++ /dev/null @@ -1,13 +0,0 @@ -description = "Generate documentation for a high level concept in the codebase" -prompt = """ - -Your primary role is to help explain this codebase and update the existing documentation with useful information for future maintainers new to the codebase. - -1. **Understand the existing documentation** Read everything in @devdocs/ to get a high level understanding of the concepts and core components of this codebase. Look for any relevant information on {{args.concept}}. -2. **Perform archeology on the codebase** Read everything in {{args.file_or_folder}} and learn more about {{args.concept}}. -3. **Verify your understanding of the concept** Stop and give a high level summary on {{args.concept}} is. Ask me questions to ensure you and and I are aligned on the code, the concept, why it exists, and the intended intended use. Create a high level summary of what you learned. Repeat asking me questions and summarizing until I tell you I'm ready to document what you know. -4. **Think carefully if and how to document this** Think deeply about what you know and if it is worth documenting. If so, think about the best way to document it. Documentation files, code comments, or both. Prefer high level documentation. -5. **Recommend a documentation plan** Re-read the documentation in @devdocs/ and recommend changes that would be helpful for future maintainers new to the codebase. Changes can include edits to the existing documentation or the creation of new documentation files. Edits to existing files must align to their purpose and level of detail. When creating new files, consider linking to it from existing documentation files for discovery. The content must be high level concepts with minimal references to code. Iterate with me until I'm satisfied with the proposed edits. - -Your final output is a recommendation to the documentation. -""" \ No newline at end of file From 99d49e34a6789ffa4416f105155297c00b8f63af Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Tue, 2 Dec 2025 16:44:50 -0800 Subject: [PATCH 15/31] Updating gemini file to be more accurate --- packages/firestore/.gemini/commands/docs/amend.toml | 13 ------------- packages/firestore/GEMINI.md | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 packages/firestore/.gemini/commands/docs/amend.toml diff --git a/packages/firestore/.gemini/commands/docs/amend.toml b/packages/firestore/.gemini/commands/docs/amend.toml deleted file mode 100644 index 13562a6044..0000000000 --- a/packages/firestore/.gemini/commands/docs/amend.toml +++ /dev/null @@ -1,13 +0,0 @@ -description = "Updates code documentation with new content" -prompt = """ - -Your primary role is to help explain this codebase and update the existing documentation with useful information for future maintainers. - -1. **Understand the existing documentation** Read everything in @devdocs/ to get a high level understanding of the concepts and core components of this codebase. Look for any relevant information on {{args.concept}}. -2. **Perform archeology on the codebase** Read everything in {{args.file_or_folder}} and learn more about {{args.concept}}. -3. **Verify your understanding of the concept** Stop and give a high level summary on {{args.concept}} is. Ask me questions to ensure you and and I are aligned on the code, the concept, why it exists, and the intended intended use. Create a high level summary of what you learned. Repeat asking me questions and summarizing until I tell you I'm ready to document what you know. -4. **Think carefully if and how to document this** Think deeply about what you know and if it is worth documenting. If so, think about the best way to document it. Documentation files, code comments, or both. Prefer high level documentation. -5. **Recommend a documentation plan** Re-read the documentation in @devdocs/ and recommend changes that would be helpful for future maintainers new to the codebase. Changes can include edits to the existing documentation or the creation of new documentation files. Edits to existing files must align to their purpose and level of detail. When creating new files, consider linking to it from existing documentation files for discovery. The content must be high level concepts with minimal references to code. Iterate with me until I'm satisfied with the proposed edits. - -Your final output is a recommendation to the documentation. -""" \ No newline at end of file diff --git a/packages/firestore/GEMINI.md b/packages/firestore/GEMINI.md index e4ce4c2123..7b4d504b39 100644 --- a/packages/firestore/GEMINI.md +++ b/packages/firestore/GEMINI.md @@ -1,5 +1,5 @@ # Firestore JavaScript SDK -This project is the official JavaScript SDK for the [Google Cloud Firestore](https://firebase.google.com/docs/firestore) database. +This folder contains the authoritative and official JavaScript SDK for the [Google Cloud Firestore](https://firebase.google.com/docs/firestore) database. You are an expert in @devdocs/prerequisites.md @devdocs/overview.md \ No newline at end of file From 85c3ad7e210ced32c66ee6215cf941066a51ad0e Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 29 Aug 2025 15:36:30 -0700 Subject: [PATCH 16/31] feat(docs): add initial developer documentation This commit introduces a new developer documentation hub under the `devdocs` directory. The goal of this new documentation is to provide a comprehensive resource for developers working on the Firestore JS SDK. The new documentation includes: - **Overview:** A high-level overview of the SDK, its goals, and architecture. - **Prerequisites:** A guide for new contributors, outlining the necessary skills and knowledge. - **Architecture:** A detailed explanation of the SDK's architecture, core components, and data flow. - **Code Layout:** A document that explains the structure of the codebase. - **Build & Testing:** Initial documents for the build and testing processes. --- packages/firestore/GEMINI.md | 2 +- packages/firestore/devdocs/architecture.md | 82 ++++++++-------------- packages/firestore/devdocs/code-layout.md | 2 +- packages/firestore/devdocs/overview.md | 2 +- 4 files changed, 34 insertions(+), 54 deletions(-) diff --git a/packages/firestore/GEMINI.md b/packages/firestore/GEMINI.md index 7b4d504b39..e4ce4c2123 100644 --- a/packages/firestore/GEMINI.md +++ b/packages/firestore/GEMINI.md @@ -1,5 +1,5 @@ # Firestore JavaScript SDK -This folder contains the authoritative and official JavaScript SDK for the [Google Cloud Firestore](https://firebase.google.com/docs/firestore) database. +This project is the official JavaScript SDK for the [Google Cloud Firestore](https://firebase.google.com/docs/firestore) database. You are an expert in @devdocs/prerequisites.md @devdocs/overview.md \ No newline at end of file diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index b5b7397c06..762f5d5ffa 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -2,41 +2,6 @@ This document provides a detailed explanation of the Firestore JavaScript SDK's architecture, its core components, and the flow of data through the system. -## Core Components - -The SDK is composed of several key components that work together to provide the full range of Firestore features. - -![Architecture Diagram](./architecture.png) - -* **API Layer**: The public-facing API surface that developers use to interact with the SDK. This layer is responsible for translating the public API calls into the internal data models and passing them to the appropriate core components. -* **Core**: - * **Event Manager**: Acts as a central hub for all eventing in the SDK. It is responsible for routing events between the API Layer and Sync Engine. It manages query listeners and is responsible for raising snapshot events, as well as handling connectivity changes and some query failures. - * **Sync Engine**: The central controller of the SDK. It acts as the glue between the Event Manager, Local Store, and Remote Store. Its responsibilities include: - * Coordinating and translating client requests and remote events from the backend. - * Initiating responses to user code from both remote events (backend updates) and local events (e.g. garbage collection). - * Managing a "view" for each query, which represents the unified view between the local and remote data stores. - * Deciding whether a document is in a "limbo" state (e.g. its state is unknown) and needs to be fetched from the backend. - * Notifying the Remote Store when the Local Store has new mutations that need to be sent to the backend. - * For web clients, synchronizing query and mutation states across multiple tabs. -* **Local Store**: A container for the components that manage persisted and in-memory data. - * **Remote Table**: A cache of the most recent version of documents as known by the Firestore backend. - * **Mutation Queue**: A queue of all the user-initiated writes (set, update, delete) that have not yet been acknowledged by the Firestore backend. - * **Local View**: A cache that represents the user's current view of the data, combining the Remote Table with the Mutation Queue. - * **Overlays**: A performance-optimizing cache that stores the calculated effect of pending mutations from the Mutation Queue on documents. Instead of re-applying mutations every time a document is read, the SDK computes this "overlay" once and caches it, allowing the Local View to be constructed more efficiently. -* **Remote Store**: The component responsible for all network communication with the Firestore backend. It manages the gRPC streams for reading and writing data, and it abstracts away the complexities of the network protocol from the rest of the SDK. -* **Persistence Layer**: The underlying storage mechanism used by the Local Store to persist data on the client. In the browser, this is implemented using IndexedDB. - -The architecture and systems within the SDK map closely to the directory structure, which helps developers navigate the codebase. Here is a mapping of the core components to their corresponding directories. - -* `src/`: - * `api/`: Implements the **API Layer** for the main SDK. - * `lite-api/`: Implements the **API Layer** for the lite SDK. - * `core/`: Implements the **Sync Engine** and **Event Manager**. - * `local/`: Implements the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, **Overlays** and the **Persistence Layer**. - * `remote/`: Implements the **Remote Store**, handling all network communication. - -For a more detailed explanation of the contents of each directory, see the [Code Layout](./code-layout.md) documentation. - ## Overview of features At a high level, all interactions with Firestore can be categorized as either reading or writing data. The SDK provides different mechanisms for these operations, each with distinct guarantees and performance characteristics. There is also a special case of writing data called tansactions detailed below. @@ -58,17 +23,39 @@ All data modifications—creates, updates, and deletes—are treated as "writes. * **One-Time Writes**: When a user performs a write (create, update, or delete), the operation is not sent directly to the backend. Instead, it's treated as a "mutation" and added to the local **Mutation Queue**. The SDK "optimistically" assumes the write will succeed on the backend and immediately reflects the change in the local view of the data, making the change visible to local queries. The SDK then works to synchronize this queue with the backend. This design is crucial for supporting offline functionality, as pending writes can be retried automatically when network connectivity is restored. -* **Transactions**: For grouping multiple write operations into a single atomic unit, the SDK provides `runTransaction`. Unlike standard writes, transactions do not use the optimistic, offline-capable write pipeline. Instead, they are sent directly to the backend, which requires an active internet connection. This ensures atomicity but means transactions do not benefit from the offline capabilities of the standard write pipeline. +* **Transactions**: This allows you to perform multiple writes as a single atomic operation. Transactions are immediately sent to the server, are **not** guaranteed consistnt with one-time writes, and bypass the other SDK systems. -### Data Bundles -A Firestore data bundle is a serialized collection of documents and query results, created on a server using the Firebase Admin SDK. Bundles are used to efficiently deliver a pre-packaged set of data to the client, which can then be loaded directly into the SDK's local cache. This is useful for: +## Core Components + +The SDK is composed of several key components that work together to provide the full range of Firestore features. + +![Architecture Diagram](./architecture.png) + +* **API Layer**: The public-facing API surface that developers use to interact with the SDK. +* **Core**: + * **Event Manager**: Acts as a central hub for all eventing in the SDK. It is responsible for routing events between the API Layer and Sync Engine. It manages query listeners and is responsible for raising snapshot events, as well as handling connectivity changes and some query failures. + * **Sync Engine**: The central controller of the SDK. It acts as the glue between the Event Manager, Local Store, and Remote Store. Its responsibilities include: + * Coordinating client requests and remote events. + * Managing a view for each query, which represents the unified view between the local and remote data stores. + * Notifying the Remote Store when the Local Store has new mutations that need to be sent to the backend. +* **Local Store**: A container for the components that manage persisted and in-memory data. + * **Remote Table**: A cache of the most recent version of documents as known by the Firestore backend. + * **Mutation Queue**: A queue of all the user-initiated writes (set, update, delete) that have not yet been acknowledged by the Firestore backend. + * **Local View**: A cache that represents the user's current view of the data, combining the Remote Table with the Mutation Queue. +* **Remote Store**: The component responsible for all network communication with the Firestore backend. +* **Persistence Layer**: The underlying storage mechanism (i.e. IndexedDB) used by the Local Store. + +The architecture and systems within the SDK map closely to the directory structure, which helps developers navigate the codebase. Here is a mapping of the core components to their corresponding directories. -* **Optimizing Server-Side Rendering (SSR)**: Bundles enable efficient hydration of client-side applications with data pre-fetched during server-side rendering, reducing initial load times and backend roundtrips. -* **Seeding initial data** for an application, allowing users to have a complete offline experience on their first use. -* **Distributing curated datasets** to clients in a single, efficient package. +* `src/`: + * `api/`: Implements the **API Layer** for the main SDK. + * `lite-api/`: Implements the **API Layer** for the lite SDK. + * `core/`: Implements the **Sync Engine** and **Event Manager**. + * `local/`: Implements the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, and the **Persistence Layer**. + * `remote/`: Implements the **Remote Store**, handling all network communication. -When a bundle is loaded, its contents are unpacked and stored in the local cache, making the data available for immediate querying without needing to connect to the Firestore backend. For more details, see the [Bundles documentation](./bundles.md). +For a more detailed explanation of the contents of each directory, see the [Code Layout](./code-layout.md) documentation. # Data Flow @@ -80,7 +67,7 @@ Here's a step-by-step walkthrough of how data flows through the SDK for a write 1. **API Layer**: A user initiates a write operation (e.g., `setDoc`, `updateDoc`, `deleteDoc`). 2. **Sync Engine**: The call is routed to the Sync Engine, which wraps the operation in a "mutation". 3. **Mutation Queue (in Local Store)**: The Sync Engine adds this mutation to the Mutation Queue. The queue is persisted to the **Persistence Layer** (IndexedDB). At this point, the SDK "optimistically" considers the write successful locally. -4. **Local View (in Local Store)**: The change is reflected in the Local View. This is done by creating or updating a cached **Overlay** for the affected document, making the change efficiently available to any active listeners without waiting for backend confirmation. +4. **Local View (in Local Store)**: The change is immediately reflected in the Local View, making it available to any active listeners without waiting for backend confirmation. 5. **Remote Store**: The Sync Engine notifies the Remote Store that there are pending mutations. 6. **Backend**: The Remote Store sends the mutations from the queue to the Firestore backend. 7. **Acknowledgement**: The backend acknowledges the write. @@ -91,7 +78,7 @@ Here's a step-by-step walkthrough of how data flows through the SDK for a write 1. **API Layer**: A user attaches a listener to a query (e.g., `onSnapshot`). 2. **Event Manager**: The Event Manager creates a listener and passes it to the Sync Engine. 3. **Sync Engine**: The Sync Engine creates a "view" for the query. -4. **Local View (in Local Store)**: The Sync Engine asks the Local Store for the current documents matching the query. The Local Store provides these by applying cached **Overlays** on top of the documents to reflect optimistic local changes from the **Mutation Queue**. +4. **Local View (in Local Store)**: The Sync Engine asks the Local Store for the current documents matching the query. This includes any optimistic local changes from the **Mutation Queue**. 5. **API Layer**: The initial data from the Local View is sent back to the user's `onSnapshot` callback. This provides a fast, initial result. 6. **Remote Store**: Simultaneously, the Sync Engine instructs the Remote Store to listen to the query on the Firestore backend. 7. **Backend**: The backend returns the initial matching documents for the query. @@ -99,10 +86,3 @@ Here's a step-by-step walkthrough of how data flows through the SDK for a write 9. **Sync Engine**: The Sync Engine is notified of the updated documents. It re-calculates the query view by combining the new data from the Remote Table with any applicable pending mutations from the **Mutation Queue**. 10. **API Layer**: If the query results have changed after this reconciliation, the new results are sent to the user's `onSnapshot` callback. This is why a listener may fire twice initially. 11. **Real-time Updates**: From now on, any changes on the backend that affect the query are pushed to the Remote Store, which updates the Remote Table, triggering the Sync Engine to re-calculate the view and notify the listener. - -## Bundle Loading Data Flow - -1. **API Layer**: The user initiates a bundle load via the public API. -2. **Sync Engine**: The Sync Engine receives the bundle and begins processing it. -3. **Local Store**: The Sync Engine unpacks the bundle and saves its contents (documents and named queries) into the **Local Store**. -4. **API Layer**: The user is notified of the progress and completion of the bundle loading operation via the task returned by the API. \ No newline at end of file diff --git a/packages/firestore/devdocs/code-layout.md b/packages/firestore/devdocs/code-layout.md index 0bdd6e4a38..619cc329e7 100644 --- a/packages/firestore/devdocs/code-layout.md +++ b/packages/firestore/devdocs/code-layout.md @@ -6,7 +6,7 @@ This document explains the code layout in this repository. It is closely related * `api/`: Implements the **API Layer** for the main SDK. * `lite-api/`: Contains the entry point of for the lite SDK. * `core/`: Contains logic for the **Sync Engine** and **Event Manager**. - * `local/`: Contains the logic the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, **Overlays**, and the **Persistence Layer**. + * `local/`: Contains the logic the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, and the **Persistence Layer**. * `remote/`: Contains the logic for the **Remote Store**, handling all network communication. * `model/`: Defines the internal data models used throughout the SDK, such as `Document`, `DocumentKey`, and `Mutation`. These models are used to represent Firestore data and operations in a structured way. * `platform/`: Contains platform-specific code to abstract away the differences between the Node.js and browser environments. This includes things like networking, storage, and timers. This allows the core logic of the SDK to be platform-agnostic. diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index 6f4f52a437..305aa77475 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -1,6 +1,6 @@ # Firestore JavaScript SDK Overview -This document is the starting point for navigating the Firestore JavaScript SDK codebase and its documentation. It provides a high-level overview of the SDK, its architecture, how it is built, tested, and the developer workflow. +This document is the starting point for navigating the Firestore JavaScript SDK codebase documentation. It provides a high-level overview of the SDK, how it is built, tested, and the developer workflow. All contributors are expected to be familiar with the [prerequisites](./prerequisites.md) before working in this codebase. From 8e07f492b0612e21c9ea077ddc4d651777d4a930 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 29 Aug 2025 15:57:03 -0700 Subject: [PATCH 17/31] feat(docs): reorganizing content --- packages/firestore/devdocs/architecture.md | 53 +++++++++++----------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index 762f5d5ffa..5bec259687 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -2,37 +2,13 @@ This document provides a detailed explanation of the Firestore JavaScript SDK's architecture, its core components, and the flow of data through the system. -## Overview of features - -At a high level, all interactions with Firestore can be categorized as either reading or writing data. The SDK provides different mechanisms for these operations, each with distinct guarantees and performance characteristics. There is also a special case of writing data called tansactions detailed below. - - -### Read Operations - -There are two fundamental ways to read data from Firestore: - -* **One-Time Reads**: This is for fetching a snapshot of data at a specific moment. It's a simple request-response model. You ask for a document or the results of a query, and the server sends back the data as it exists at that instant. - -* **Real-Time Listeners**: This allows you to subscribe to a document or a query. The server first sends you the initial data and then continues to push updates to your client in real time as the data changes. This is the foundation of Firestore's real-time capabilities. - -When a query is executed, the SDK immediately returns data from the local cache, which includes any pending optimistic writes from the **Mutation Queue**. This provides a fast, responsive experience. At the same time, the SDK sends the query to the Firestore backend to fetch the latest version of the documents. When the fresh documents arrive from the backend, the SDK takes these server-authoritative documents and re-applies any pending mutations from the local queue on top of them. It then re-runs the original query against this newly merged data. If the documents still match the query's criteria, they are delivered to the query listener again. This is a common occurrence and means a listener could see an event for the same document twice: first with the cached, optimistic data, and a second time after the backend data is reconciled. - -### Write Operations - -All data modifications—creates, updates, and deletes—are treated as "writes." The SDK is designed to make writes atomic and resilient. There are two fundamental ways to write data to Firestore: - -* **One-Time Writes**: When a user performs a write (create, update, or delete), the operation is not sent directly to the backend. Instead, it's treated as a "mutation" and added to the local **Mutation Queue**. The SDK "optimistically" assumes the write will succeed on the backend and immediately reflects the change in the local view of the data, making the change visible to local queries. The SDK then works to synchronize this queue with the backend. This design is crucial for supporting offline functionality, as pending writes can be retried automatically when network connectivity is restored. - -* **Transactions**: This allows you to perform multiple writes as a single atomic operation. Transactions are immediately sent to the server, are **not** guaranteed consistnt with one-time writes, and bypass the other SDK systems. - - ## Core Components The SDK is composed of several key components that work together to provide the full range of Firestore features. ![Architecture Diagram](./architecture.png) -* **API Layer**: The public-facing API surface that developers use to interact with the SDK. +* **API Layer**: The public-facing API surface that developers use to interact with the SDK. This layer is responsible for translating the public API calls into the internal data models and passing them to the appropriate core components. * **Core**: * **Event Manager**: Acts as a central hub for all eventing in the SDK. It is responsible for routing events between the API Layer and Sync Engine. It manages query listeners and is responsible for raising snapshot events, as well as handling connectivity changes and some query failures. * **Sync Engine**: The central controller of the SDK. It acts as the glue between the Event Manager, Local Store, and Remote Store. Its responsibilities include: @@ -43,8 +19,8 @@ The SDK is composed of several key components that work together to provide the * **Remote Table**: A cache of the most recent version of documents as known by the Firestore backend. * **Mutation Queue**: A queue of all the user-initiated writes (set, update, delete) that have not yet been acknowledged by the Firestore backend. * **Local View**: A cache that represents the user's current view of the data, combining the Remote Table with the Mutation Queue. -* **Remote Store**: The component responsible for all network communication with the Firestore backend. -* **Persistence Layer**: The underlying storage mechanism (i.e. IndexedDB) used by the Local Store. +* **Remote Store**: The component responsible for all network communication with the Firestore backend. It manages the gRPC streams for reading and writing data, and it abstracts away the complexities of the network protocol from the rest of the SDK. +* **Persistence Layer**: The underlying storage mechanism used by the Local Store to persist data on the client. In the browser, this is implemented using IndexedDB. The architecture and systems within the SDK map closely to the directory structure, which helps developers navigate the codebase. Here is a mapping of the core components to their corresponding directories. @@ -57,6 +33,29 @@ The architecture and systems within the SDK map closely to the directory structu For a more detailed explanation of the contents of each directory, see the [Code Layout](./code-layout.md) documentation. +## Overview of features + +At a high level, all interactions with Firestore can be categorized as either reading or writing data. The SDK provides different mechanisms for these operations, each with distinct guarantees and performance characteristics. There is also a special case of writing data called tansactions detailed below. + + +### Read Operations + +There are two fundamental ways to read data from Firestore: + +* **One-Time Reads**: This is for fetching a snapshot of data at a specific moment. It's a simple request-response model. You ask for a document or the results of a query, and the server sends back the data as it exists at that instant. + +* **Real-Time Listeners**: This allows you to subscribe to a document or a query. The server first sends you the initial data and then continues to push updates to your client in real time as the data changes. This is the foundation of Firestore's real-time capabilities. + +When a query is executed, the SDK immediately returns data from the local cache, which includes any pending optimistic writes from the **Mutation Queue**. This provides a fast, responsive experience. At the same time, the SDK sends the query to the Firestore backend to fetch the latest version of the documents. When the fresh documents arrive from the backend, the SDK takes these server-authoritative documents and re-applies any pending mutations from the local queue on top of them. It then re-runs the original query against this newly merged data. If the documents still match the query's criteria, they are delivered to the query listener again. This is a common occurrence and means a listener could see an event for the same document twice: first with the cached, optimistic data, and a second time after the backend data is reconciled. + +### Write Operations + +All data modifications—creates, updates, and deletes—are treated as "writes." The SDK is designed to make writes atomic and resilient. There are two fundamental ways to write data to Firestore: + +* **One-Time Writes**: When a user performs a write (create, update, or delete), the operation is not sent directly to the backend. Instead, it's treated as a "mutation" and added to the local **Mutation Queue**. The SDK "optimistically" assumes the write will succeed on the backend and immediately reflects the change in the local view of the data, making the change visible to local queries. The SDK then works to synchronize this queue with the backend. This design is crucial for supporting offline functionality, as pending writes can be retried automatically when network connectivity is restored. + +* **Transactions**: For grouping multiple write operations into a single atomic unit, the SDK provides `runTransaction`. Unlike standard writes, transactions do not use the optimistic, offline-capable write pipeline. Instead, they are sent directly to the backend, which requires an active internet connection. This ensures atomicity but means transactions do not benefit from the offline capabilities of the standard write pipeline. + # Data Flow From ed5057b8fc59cfedcd589fa88dd372c33f77d07b Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Mon, 15 Dec 2025 16:32:01 -0800 Subject: [PATCH 18/31] docs: Adding details from life of a query training video --- packages/firestore/devdocs/architecture.md | 12 ++++-- packages/firestore/devdocs/code-layout.md | 7 +++- packages/firestore/devdocs/overview.md | 10 +++++ packages/firestore/devdocs/query-execution.md | 38 +++++++++++++++++++ packages/firestore/devdocs/testing.md | 5 +++ 5 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 packages/firestore/devdocs/query-execution.md diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index 5bec259687..25e0afe5ff 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -12,13 +12,17 @@ The SDK is composed of several key components that work together to provide the * **Core**: * **Event Manager**: Acts as a central hub for all eventing in the SDK. It is responsible for routing events between the API Layer and Sync Engine. It manages query listeners and is responsible for raising snapshot events, as well as handling connectivity changes and some query failures. * **Sync Engine**: The central controller of the SDK. It acts as the glue between the Event Manager, Local Store, and Remote Store. Its responsibilities include: - * Coordinating client requests and remote events. - * Managing a view for each query, which represents the unified view between the local and remote data stores. + * Coordinating and translating client requests and remote events from the backend. + * Initiating responses to user code from both remote events (backend updates) and local events (e.g. garbage collection). + * Managing a "view" for each query, which represents the unified view between the local and remote data stores. The Sync Engine builds the user-facing "View" using the formula: `View = Remote Document + Overlay`. A **Remote Document** is the authoritative state from the backend. An **Overlay** is A computed "delta" representing pending local mutations. Overlays are calculated immediately when a mutation is applied and persisted separately. This allows for zero-latency "Optimistic Updates." + * Deciding whether a document is in a "limbo" state (e.g. its state is unknown) and needs to be fetched from the backend. * Notifying the Remote Store when the Local Store has new mutations that need to be sent to the backend. * **Local Store**: A container for the components that manage persisted and in-memory data. - * **Remote Table**: A cache of the most recent version of documents as known by the Firestore backend. + * **Remote Table**: A cache of the most recent version of documents as known by the Firestore backend (A.K.A. Remote Documents). * **Mutation Queue**: A queue of all the user-initiated writes (set, update, delete) that have not yet been acknowledged by the Firestore backend. * **Local View**: A cache that represents the user's current view of the data, combining the Remote Table with the Mutation Queue. + * **Query Engine**: Determines the most efficient strategy (Index vs. Scan) to identify documents matching a query in the local cache. + * **Overlays**: A performance-optimizing cache that stores the calculated effect of pending mutations from the Mutation Queue on documents. Instead of re-applying mutations every time a document is read, the SDK computes this "overlay" once and caches it, allowing the Local View to be constructed more efficiently. * **Remote Store**: The component responsible for all network communication with the Firestore backend. It manages the gRPC streams for reading and writing data, and it abstracts away the complexities of the network protocol from the rest of the SDK. * **Persistence Layer**: The underlying storage mechanism used by the Local Store to persist data on the client. In the browser, this is implemented using IndexedDB. @@ -77,7 +81,7 @@ Here's a step-by-step walkthrough of how data flows through the SDK for a write 1. **API Layer**: A user attaches a listener to a query (e.g., `onSnapshot`). 2. **Event Manager**: The Event Manager creates a listener and passes it to the Sync Engine. 3. **Sync Engine**: The Sync Engine creates a "view" for the query. -4. **Local View (in Local Store)**: The Sync Engine asks the Local Store for the current documents matching the query. This includes any optimistic local changes from the **Mutation Queue**. +4. **Local View (in Local Store)**: The Sync Engine asks the Query Engine in the Local Store to execute the query. The Query Engine selects a strategy (e.g., Index Scan or Timestamp Optimization) to find matching keys. The Local Store then constructs the documents by applying cached Overlays on top of Remote Documents. 5. **API Layer**: The initial data from the Local View is sent back to the user's `onSnapshot` callback. This provides a fast, initial result. 6. **Remote Store**: Simultaneously, the Sync Engine instructs the Remote Store to listen to the query on the Firestore backend. 7. **Backend**: The backend returns the initial matching documents for the query. diff --git a/packages/firestore/devdocs/code-layout.md b/packages/firestore/devdocs/code-layout.md index 619cc329e7..59a00c1a4d 100644 --- a/packages/firestore/devdocs/code-layout.md +++ b/packages/firestore/devdocs/code-layout.md @@ -6,7 +6,12 @@ This document explains the code layout in this repository. It is closely related * `api/`: Implements the **API Layer** for the main SDK. * `lite-api/`: Contains the entry point of for the lite SDK. * `core/`: Contains logic for the **Sync Engine** and **Event Manager**. - * `local/`: Contains the logic the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, and the **Persistence Layer**. + * `local/`: Contains the logic the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, **Overlays**, and the **Persistence Layer** + * `local_store.ts`: The main entry point for persistence operations. + * `query_engine.ts`: Implements the strategy selection logic (Scan vs. Index). + * `index_backfiller.ts`: The background task that updates Client-Side Indexes. + * `remote_document_cache.ts`: Manages the `remote_documents` table (base truth). + * `overlay_cache.ts`: Manages pending mutation queue. * `remote/`: Contains the logic for the **Remote Store**, handling all network communication. * `model/`: Defines the internal data models used throughout the SDK, such as `Document`, `DocumentKey`, and `Mutation`. These models are used to represent Firestore data and operations in a structured way. * `platform/`: Contains platform-specific code to abstract away the differences between the Node.js and browser environments. This includes things like networking, storage, and timers. This allows the core logic of the SDK to be platform-agnostic. diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index 305aa77475..499a7444d5 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -16,6 +16,16 @@ The primary goals of this SDK are: * Offer a lightweight version for applications that do not require advanced features. * Maintain API and architectural symmetry with the [Firestore Android SDK](https://github.com/firebase/firebase-android-sdk) and [Firestore iOS SDK](https://github.com/firebase/firebase-ios-sdk). This consistency simplifies maintenance and makes it easier to port features between platforms. The public API is intentionally consistent across platforms, even if it means being less idiomatic, to allow developers to more easily port their application code. + +## Key Concepts & Vocabulary + +* **Query**: The client-side representation of a data request (filters, order bys). +* **Target**: The backend's representation of a Query. The SDK allocates a unique integer `TargetID` for every unique query to manage the real-time stream. +* **Mutation**: A user-initiated change (Set, Update, Delete). Mutations are queued locally and sent to the backend. +* **Overlay**: The computed result of applying a Mutation to a Document. We store these to show "Optimistic Updates" instantly without modifying the underlying "Remote Document" until the server confirms the write. +* **Limbo**: A state where a document exists locally and matches a query, but the server hasn't explicitly confirmed it belongs to the current snapshot version. The SDK must perform "Limbo Resolution" to ensure these documents are valid. + + ## Artifacts The Firestore JavaScript SDK is divided into two main packages: diff --git a/packages/firestore/devdocs/query-execution.md b/packages/firestore/devdocs/query-execution.md new file mode 100644 index 0000000000..79c3b65a22 --- /dev/null +++ b/packages/firestore/devdocs/query-execution.md @@ -0,0 +1,38 @@ +# Query Execution & Indexing + +This document details how the Firestore SDK executes queries against the local cache. Understanding this is crucial for debugging performance issues and understanding offline behavior. + +## The Query Engine + +The **Query Engine** is the component within the **Local Store** responsible for finding the set of document keys that match a given query. It does not load the full document data; it only identifies the keys. It employs a hierarchy of strategies, ordered by efficiency: + +1. **Full Index Scan (O(log N))**: + * Used when a Client-Side Index (CSI) exists that covers all filters and sort orders of the query. + * This is the most performant strategy. +2. **Partial Index Scan**: + * Used when an index covers some filters (typically equality filters like `where('status', '==', 'published')`). + * The engine uses the index to narrow down the potential keys and then performs a scan on that smaller subset to verify the remaining conditions. +3. **Index-Free (Timestamp) Optimization**: + * **Concept**: If the client has been online and syncing, it knows the state of the collection up to a specific point in time (the `lastLimboFreeSnapshot`). + * **Mechanism**: The SDK assumes the "base state" (documents matching at the last snapshot) is correct. It then only scans the `remote_documents` table for documents modified *after* that snapshot version. + * This drastically reduces the work required for active listeners, changing the cost from *Collection Size* to *Recent Change Volume*. +4. **Full Collection Scan (O(N))**: + * The fallback strategy. The engine iterates through every document in the collection locally to check for matches. + +## Client-Side Indexing (CSI) + +To support efficient querying without blocking the main thread, the SDK utilizes a decoupled indexing architecture. + +* **Structure**: Index entries are stored in a dedicated `index_entries` table. An entry maps field values (e.g., `(coll/doc1, fieldA=1)`) to a document key. +* **The Index Backfiller**: Indexes are **not** updated synchronously when you write a document. Instead, a background task called the **Backfiller** runs periodically (when the SDK is idle). It reads new/modified documents and updates the index entries. +* **Hybrid Lookup**: Because the Backfiller is asynchronous, the index might be "stale" (behind the document cache). To guarantee consistency, the Query Engine performs a **Hybrid Lookup**: + 1. Query the **Index** for results up to the `IndexOffset` (the point where the Backfiller stopped). + 2. Query the **Remote Document Cache** for any documents modified *after* the `IndexOffset`. + 3. Merge the results. + +## Composite Queries (OR / IN) + +Queries using `OR` or `IN` are not executed as a single monolithic scan. The SDK transforms these into **Disjunctive Normal Form (DNF)**—essentially breaking them into multiple sub-queries. + +* **Execution**: Each sub-query is executed independently using the strategies above (Index vs. Scan). +* **Union**: The resulting sets of document keys are unioned together in memory to produce the final result. \ No newline at end of file diff --git a/packages/firestore/devdocs/testing.md b/packages/firestore/devdocs/testing.md index 11f00ebca8..235dde9a43 100644 --- a/packages/firestore/devdocs/testing.md +++ b/packages/firestore/devdocs/testing.md @@ -9,6 +9,11 @@ This document provides a detailed explanation of the Firestore JavaScript SDK te - Firebase emulator for local development - Integration testing with the backend +## Component Testing +* **Query Engine Tests**: We rely on specific spec tests to ensure the Query Engine picks the correct strategy (e.g., verifying that it uses an Index when available rather than scanning). +* **Integration Tests**: Use the Firebase Emulator to verify that "Limbo Resolution" correctly fetches documents when the local cache drifts from the server state. + + # Patterns and Practices From 2d18b56f0a2d74333c964ad143373a6d8cd8364f Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Wed, 17 Dec 2025 13:22:59 -0800 Subject: [PATCH 19/31] docs: Adding life of a query --- packages/firestore/devdocs/architecture.md | 16 ++- packages/firestore/devdocs/code-layout.md | 33 +++--- packages/firestore/devdocs/overview.md | 30 ++--- .../firestore/devdocs/persistence-schema.md | 55 +++++++++ packages/firestore/devdocs/query-execution.md | 2 +- packages/firestore/devdocs/query-lifecycle.md | 105 ++++++++++++++++++ 6 files changed, 206 insertions(+), 35 deletions(-) create mode 100644 packages/firestore/devdocs/persistence-schema.md create mode 100644 packages/firestore/devdocs/query-lifecycle.md diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index 25e0afe5ff..bd73cc0142 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -11,18 +11,20 @@ The SDK is composed of several key components that work together to provide the * **API Layer**: The public-facing API surface that developers use to interact with the SDK. This layer is responsible for translating the public API calls into the internal data models and passing them to the appropriate core components. * **Core**: * **Event Manager**: Acts as a central hub for all eventing in the SDK. It is responsible for routing events between the API Layer and Sync Engine. It manages query listeners and is responsible for raising snapshot events, as well as handling connectivity changes and some query failures. - * **Sync Engine**: The central controller of the SDK. It acts as the glue between the Event Manager, Local Store, and Remote Store. Its responsibilities include: - * Coordinating and translating client requests and remote events from the backend. - * Initiating responses to user code from both remote events (backend updates) and local events (e.g. garbage collection). - * Managing a "view" for each query, which represents the unified view between the local and remote data stores. The Sync Engine builds the user-facing "View" using the formula: `View = Remote Document + Overlay`. A **Remote Document** is the authoritative state from the backend. An **Overlay** is A computed "delta" representing pending local mutations. Overlays are calculated immediately when a mutation is applied and persisted separately. This allows for zero-latency "Optimistic Updates." - * Deciding whether a document is in a "limbo" state (e.g. its state is unknown) and needs to be fetched from the backend. - * Notifying the Remote Store when the Local Store has new mutations that need to be sent to the backend. +* **Sync Engine**: The central controller of the SDK. It acts as the glue between the Event Manager, Local Store, and Remote Store. + * **Coordinator**: It bridges the **User World** (Query) and **System World** (Target), converting public API calls into internal `TargetIDs`. + * **View Construction**: It manages the user-facing view using the formula: `View = Remote Document + Overlay`. + * **Remote Document**: The authoritative state from the backend. + * **Overlay**: A computed delta representing pending local mutations. + * **Limbo Resolution**: It detects "Limbo" documents (local matches not confirmed by server) and initiates resolution flows to verify their existence. + * **Lifecycle Management**: It controls the [Query Lifecycle](./query-lifecycle.md), managing the initialization of streams, the persistence of data, and garbage collection eligibility. * **Local Store**: A container for the components that manage persisted and in-memory data. * **Remote Table**: A cache of the most recent version of documents as known by the Firestore backend (A.K.A. Remote Documents). * **Mutation Queue**: A queue of all the user-initiated writes (set, update, delete) that have not yet been acknowledged by the Firestore backend. * **Local View**: A cache that represents the user's current view of the data, combining the Remote Table with the Mutation Queue. * **Query Engine**: Determines the most efficient strategy (Index vs. Scan) to identify documents matching a query in the local cache. * **Overlays**: A performance-optimizing cache that stores the calculated effect of pending mutations from the Mutation Queue on documents. Instead of re-applying mutations every time a document is read, the SDK computes this "overlay" once and caches it, allowing the Local View to be constructed more efficiently. + * For a detailed breakdown of the IndexedDB structure and tables, see [Persistence Schema](./persistence-schema.md). * **Remote Store**: The component responsible for all network communication with the Firestore backend. It manages the gRPC streams for reading and writing data, and it abstracts away the complexities of the network protocol from the rest of the SDK. * **Persistence Layer**: The underlying storage mechanism used by the Local Store to persist data on the client. In the browser, this is implemented using IndexedDB. @@ -89,3 +91,5 @@ Here's a step-by-step walkthrough of how data flows through the SDK for a write 9. **Sync Engine**: The Sync Engine is notified of the updated documents. It re-calculates the query view by combining the new data from the Remote Table with any applicable pending mutations from the **Mutation Queue**. 10. **API Layer**: If the query results have changed after this reconciliation, the new results are sent to the user's `onSnapshot` callback. This is why a listener may fire twice initially. 11. **Real-time Updates**: From now on, any changes on the backend that affect the query are pushed to the Remote Store, which updates the Remote Table, triggering the Sync Engine to re-calculate the view and notify the listener. + +**Note on Query Lifecycle:** The steps above describe the "Happy Path" of a query starting up. For details on how queries are deduplicated, how the data persists after a listener is removed, and how Garbage Collection eventually cleans it up, see the [Query Lifecycle](query-lifecycle.md). \ No newline at end of file diff --git a/packages/firestore/devdocs/code-layout.md b/packages/firestore/devdocs/code-layout.md index 59a00c1a4d..d46cb3a194 100644 --- a/packages/firestore/devdocs/code-layout.md +++ b/packages/firestore/devdocs/code-layout.md @@ -3,19 +3,26 @@ This document explains the code layout in this repository. It is closely related to the [architecture](./architecture.md). * `src/`: Contains the source code for the main `@firebase/firestore` package. - * `api/`: Implements the **API Layer** for the main SDK. - * `lite-api/`: Contains the entry point of for the lite SDK. - * `core/`: Contains logic for the **Sync Engine** and **Event Manager**. - * `local/`: Contains the logic the **Local Store**, which includes the **Mutation Queue**, **Remote Table**, **Local View**, **Overlays**, and the **Persistence Layer** - * `local_store.ts`: The main entry point for persistence operations. - * `query_engine.ts`: Implements the strategy selection logic (Scan vs. Index). - * `index_backfiller.ts`: The background task that updates Client-Side Indexes. - * `remote_document_cache.ts`: Manages the `remote_documents` table (base truth). - * `overlay_cache.ts`: Manages pending mutation queue. - * `remote/`: Contains the logic for the **Remote Store**, handling all network communication. - * `model/`: Defines the internal data models used throughout the SDK, such as `Document`, `DocumentKey`, and `Mutation`. These models are used to represent Firestore data and operations in a structured way. - * `platform/`: Contains platform-specific code to abstract away the differences between the Node.js and browser environments. This includes things like networking, storage, and timers. This allows the core logic of the SDK to be platform-agnostic. - * `protos/`: Contains the Protocol Buffer (`.proto`) definitions that describe the gRPC API surface of the Firestore backend. These files are used to generate the client-side networking code. + * `api/`: **API Surface**. Implements the public API (e.g., `doc`, `collection`, `onSnapshot`). + * `database.ts`: The entry point for the `Firestore` class. + * `reference.ts`: Implements `DocumentReference` and `CollectionReference`. + * `core/`: **Sync Engine**. Contains the high-level orchestration logic. + * `sync_engine.ts`: The central coordinator. It manages the "User World" <-> "System World" bridge, `TargetID` allocation, and the main async queue. + * `event_manager.ts`: Handles `QueryListener` registration, fan-out (deduplication of identical queries), and raising snapshot events to the user. + * `query.ts`: Defines the internal `Query` and `Target` models. + * `firestore_client.ts`: The initialization logic that wires up the components. + * `local/`: **Storage and Query Execution**. Manages persistence, caching, and local execution. + * `local_store.ts`: The main interface for the Core layer to interact with storage. It coordinates the components below. + * `indexeddb_persistence.ts`: The implementation of the [Persistence Schema](./persistence-schema.md) using IndexedDB. + * `local_documents_view.ts`: Implements the logic to assemble the user-facing view (`RemoteDoc` + `Mutation`). + * `query_engine.ts`: The optimizer that decides how to scan the cache. + * `lru_garbage_collector.ts` & `reference_delegate.ts`: Implements the Sequence Number logic to clean up old data. + * `remote/`: **Network**. Handles gRPC/REST communication. + * `remote_store.ts`: Manages the "Watch Stream" (listening to queries) and the "Commit Stream" (sending mutations). + * `connection.ts`: Abstracts the underlying networking transport. + * `serializer.ts`: Converts between internal model objects and the Protobuf format used by the backend. + * `model/`: Defines the immutable data structures used throughout the SDK (e.g., `DocumentKey`, `FieldPath`, `Mutation`). + * `util/`: General purpose utilities (AsyncQueue, Assertions, Types). * `lite/`: Defines the entrypoint code for the `@firebase/firestore/lite` package. * `test/`: Contains all unit and integration tests for the SDK. The tests are organized by component and feature, and they are essential for ensuring the quality and correctness of the code. * `scripts/`: Contains a collection of build and maintenance scripts used for tasks such as bundling the code, running tests, and generating documentation. diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index 499a7444d5..07a1007884 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -25,6 +25,7 @@ The primary goals of this SDK are: * **Overlay**: The computed result of applying a Mutation to a Document. We store these to show "Optimistic Updates" instantly without modifying the underlying "Remote Document" until the server confirms the write. * **Limbo**: A state where a document exists locally and matches a query, but the server hasn't explicitly confirmed it belongs to the current snapshot version. The SDK must perform "Limbo Resolution" to ensure these documents are valid. +For a detailed explanation of how these concepts interact during execution, see the [Query Lifecycle](./query-lifecycle.md) documentation. ## Artifacts @@ -33,23 +34,22 @@ The Firestore JavaScript SDK is divided into two main packages: * `@firebase/firestore`: The main, full-featured SDK that provides streaming and offline support. * `@firebase/firestore/lite`: A much lighter-weight (AKA "lite") version of the SDK for applications that do not require streaming or offline support. -For a detailed explanation of the architecture, components, and data flow, please see the [Architecture documentation](./architecture.md). Related, for a deailed overview of the source code layout, please see [Code layout](./code-layout.md). +## Documentation Map -## Build +To navigate the internals of the SDK, use the following guide: -TODO: Add critical information about the build process including optimizations for code size, etc. +### Core Concepts +* **[Architecture](./architecture.md)**: The high-level block diagram of the system (API -> Core -> Local -> Remote). +* **[Query Lifecycle](./query-lifecycle.md)**: The state machine of a query. **Read this** to understand how querying and offline capabilities work. -For information on how the artifacts are built, please see the [Build documentation](./build.md) file. +### Subsystem Deep Dives +* **[Persistence Schema](./persistence-schema.md)**: A reference guide for the IndexedDB tables (e.g., `remote_documents`, `mutation_queues`). +* **[Query Execution](./query-execution.md)**: Details on the algorithms used by the Local Store to execute queries (Index Scans vs. Full Collection Scans). +* **[Bundles](./bundles.md)**: How the SDK loads and processes data bundles. -## Testing - -TODO: Add critical information about the tests harness, organization, spec tests, etc. - -For information on how the tests are setup and organized [Testing documentation](./testing.md) file. - -## Developer Workflow - -TODO: Add list of common commands here. - -For information on the developer workflow, including how to build, test, and format the code, please see the [CONTRIBUTING.md](../CONTRIBUTING.md) file. +### Developer Guides +* **[Code Layout](./code-layout.md)**: Maps the architectural components to specific source files and directories. +* **[Build Process](./build.md)**: How to build the artifacts. +* **[Testing](./testing.md)**: How to run unit and integration tests. +* **[Contributing](../CONTRIBUTING.md)**: How to run unit and integration tests. \ No newline at end of file diff --git a/packages/firestore/devdocs/persistence-schema.md b/packages/firestore/devdocs/persistence-schema.md new file mode 100644 index 0000000000..53cb893632 --- /dev/null +++ b/packages/firestore/devdocs/persistence-schema.md @@ -0,0 +1,55 @@ +# Persistence Schema (IndexedDB) + +The Firestore JS SDK persists data to IndexedDB to support offline querying, latency compensation, and app restarts. + +While the Android/iOS SDKs use SQLite, the JS SDK uses IndexedDB Object Stores. However, the logical schema is identical across platforms to ensure consistent behavior. + +## Core Object Stores + +### `remote_documents` +* **Concept**: The client's cache of the backend's "Source of Truth." +* **Key**: `DocumentKey` (Path to the document). +* **Value**: + * **Document Data**: The serialized Protobuf of the document. + * **ReadTime**: The snapshot version at which this document was read. +* **Note**: This store **never** contains local, unacknowledged writes. It only contains data confirmed by the server. To see what the developer sees, we overlay the `mutation_queues` on top of this. + +### `mutation_queues` +* **Concept**: The "Pending Writes" queue. +* **Key**: `BatchID` (Integer, auto-incrementing). +* **Grouping**: Queues are partitioned by **UID**. When a developer logs out, the SDK switches to a different queue. +* **Value**: + * **Mutation**: The serialized operation (Set, Patch, Delete). + * **Metadata**: Timestamp, offsets. +* **Behavior**: When the network is available, the `RemoteStore` reads from this queue to send write batches to the backend. Once acknowledged, entries are removed. + +### `targets` +* **Concept**: Metadata about active and cached queries. +* **Key**: `TargetID` (Internal Integer allocated by `SyncEngine`). +* **Value**: + * **Canonical ID**: A hash string representing the query (filters, sort order). Used for deduplication. + * **Resume Token**: An opaque token from the backend used to resume a stream without re-downloading all data. + * **Last Sequence Number**: Used for Garbage Collection (LRU). + +### `target_documents` (The Index) +* **Concept**: A reverse index mapping `TargetID` $\leftrightarrow$ `DocumentKey`. +* **Purpose**: Optimization. When a query is executed locally, the SDK uses this index to quickly identify which documents belong to a specific TargetID without scanning the entire `remote_documents` table. +* **Maintenance**: This is updated whenever a remote snapshot adds/removes a document from a query view. + +## Metadata & Garbage Collection Stores + +### `target_globals` +* **Concept**: A singleton store for global system state. +* **Key**: Fixed singleton key. +* **Value**: + * **`last_sequence_number`**: A global integer counter incremented on every transaction. + * **`target_count`**: Number of targets currently tracked. + +### `remote_document_changes` (Ephemeral) +* **Concept**: A temporary staging area used during `SyncEngine` processing. +* **Purpose**: Used to track read-time updates for documents during a remote event application before they are committed to the main `remote_documents` store. + +## Data Relationships + +1. **The "View"**: To construct a document for the developer, the SDK reads `remote_documents[key]` and applies any mutations found in `mutation_queues` matching that key. +2. **Garbage Collection**: The `LruGarbageCollector` uses `target_globals.last_sequence_number` and `targets.last_sequence_number` to determine which targets are old and can be evicted. It then uses `target_documents` to find which documents are no longer referenced by *any* target and deletes them from `remote_documents`. \ No newline at end of file diff --git a/packages/firestore/devdocs/query-execution.md b/packages/firestore/devdocs/query-execution.md index 79c3b65a22..c28bedaf7f 100644 --- a/packages/firestore/devdocs/query-execution.md +++ b/packages/firestore/devdocs/query-execution.md @@ -1,6 +1,6 @@ # Query Execution & Indexing -This document details how the Firestore SDK executes queries against the local cache. Understanding this is crucial for debugging performance issues and understanding offline behavior. +*Note: This document details the internal algorithms used during **View Calculation** of the [Query Lifecycle](./query-lifecycle.md). It focuses on the performance and mechanics of the **Local Store**.* ## The Query Engine diff --git a/packages/firestore/devdocs/query-lifecycle.md b/packages/firestore/devdocs/query-lifecycle.md new file mode 100644 index 0000000000..056f8f5012 --- /dev/null +++ b/packages/firestore/devdocs/query-lifecycle.md @@ -0,0 +1,105 @@ +# Query Lifecycle & Persistence State + +This document details the internal state machine of a Firestore query from creation to garbage collection. While [architecture.md](./architecture.md) covers the high-level flow of data, this document focuses on the management of internal resources, specifically the distinction between user-facing `Queries` and system-facing `Targets`. + +## Key Concepts + +* **Query**: The immutable public object representing the user's request (e.g., `coll.where("status", "==", "online")`). +* **Target**: The internal representation of a Query sent to the backend. It is assigned a unique integer `TargetID`. +* **TargetData**: Metadata persisted about a Target in the `targets` table, including its `resumeToken`, `snapshotVersion`, and `lastListenSequenceNumber`. +* **Overlay**: A computed data structure representing a local mutation (Set/Patch) that has not yet been synced to the server. The SDK "overlays" this on top of the Remote Document to calculate the user's view. +* **Limbo**: A synchronization state where a document exists locally and matches a query, but the backend hasn't explicitly confirmed it exists in the current snapshot version. + +--- + +## Phase 1: Fan-Out & Deduplication (Event Manager) + +When a user calls `onSnapshot(query, callback)`, the **Event Manager** does not immediately spawn a network request. + +1. **Deduplication**: The Event Manager calculates the "Canonical ID" of the query to check if an identical `Query` is already active in the system. +2. **Fan-Out**: + * **If Active**: The new listener is attached to the existing `QueryListener`. The current cached view is returned immediately via `fromCache: true`. + * **If New**: The Event Manager forwards the request to the **Sync Engine**. + +## Phase 2: Target Allocation (Sync Engine) + +The **Sync Engine** acts as the coordinator between the User World (Query) and the System World (Target). + +1. **Allocation**: The Sync Engine asks the **Local Store** to allocate a `TargetID` for the query. +2. **Persistence**: The `TargetData` is written to the `targets` table (IndexedDB) with an initial `sequence_number`. +3. **Watch Stream**: The Sync Engine instructs the **Remote Store** to begin listening to this `TargetID` over the network. + +## Phase 3: The "View" Calculation (Local Store) + +The "View" is what the user actually sees via the API. It is constructed purely from local data, allowing for offline access and optimistic updates. + +> **Formula:** `View = RemoteDocuments (Cache) + Overlays (Pending Writes)` + +When a query runs (initially or after a remote update), the **Local Store** performs the following: + +1. **Execution**: The `Query Engine` determines the most efficient strategy to find the set of matching `DocumentKeys`. + * *Deep Dive*: For details on Index Scans, Full Scans, and Optimization strategies, see [Query Execution & Indexing](./query-execution.md). +2. **Base State**: The store retrieves the confirmed server state from the `remote_documents` table. +3. **Overlay Application**: + * The store checks the `mutation_queues` for any pending writes associated with these keys. + * These mutations are converted into **Overlays**. + * The Overlay is applied on top of the Remote Document. +4. **Projection**: The final composed documents are sent to the Event Manager. + +*Note: This design ensures users always see their own pending writes immediately (Latency Compensation), even if the backend has not acknowledged them.* + +## Phase 4: Synchronization & Limbo Resolution + +As the **Remote Store** receives snapshot events from the backend, the Sync Engine reconciles the state. This usually follows a "Happy Path," but occasionally encounters "Limbo." + +### The Happy Path +1. Backend sends a `DocumentChange`. +2. Sync Engine updates `remote_documents` table. +3. Local Store recalculates the View. +4. If the view changes, Event Manager fires the user callback. + +### The Limbo State +Limbo occurs when the local cache believes a document matches a query, but the server snapshot says otherwise (or is silent about it). + +1. **Detection**: During snapshot processing, the Sync Engine notices a locally matching document is missing from the server's update. The document enters **Limbo**. +2. **Resolution**: The Sync Engine implicitly spins up a secondary "Limbo Resolution" listener specifically for that document's key. +3. **Verification**: + * If the server confirms the document exists: It is updated in `remote_documents` and stays in the view. + * If the server returns "Not Found" or the document no longer matches: It is removed from the local cache and the view. + +## Phase 5: Teardown (Stop Listening) + +When a user calls `unsubscribe()`, data is **not** immediately deleted. + +1. **Event Manager**: Decrements the listener count for that Query. +2. **Zero-Count**: If the count hits 0, the Event Manager notifies the Sync Engine. +3. **Sync Engine**: + * Removes the `TargetID` from the Remote Store (stopping the network stream). + * **Crucial**: The data in `remote_documents` and the metadata in `targets` remains on disk. This allows for "Offline Query Acceleration" if the user restarts the app or re-issues the query later. + +## Phase 6: Garbage Collection (The "Death" of a Query) + +Since data persists after `unsubscribe()`, the SDK must actively manage disk usage. This is handled by the **LruGarbageCollector**. + +### Sequence Numbers +Every transaction (write, listen, update) increments a global `last_sequence_number` in the `target_globals` table. +* **Active Targets**: When a query is listened to, its `target` entry is updated with the current global sequence number. +* **Inactive Targets**: Old queries retain older sequence numbers. + +### The GC Process +When the SDK detects memory/disk pressure (or periodically): +1. **Reference Delegate**: Scans the `target_documents` reverse index. +2. **Orphan Check**: It identifies documents that belong *only* to Targets that are: + * Inactive (0 listeners). + * Old (Sequence number is below the GC threshold). +3. **Purge**: + * The document is deleted from `remote_documents`. + * The `Target` metadata is eventually removed from the `targets` table. + +## Debugging Tips + +If you are debugging a **"Zombie Document"** (data appearing that should be gone) or **"Missing Data"**: + +1. **Check `targets`**: Is there an active target (valid `resumeToken`) covering that document? +2. **Check `mutation_queues`**: Is there a pending mutation (BatchID) that hasn't been acknowledged? This creates an Overlay that persists even if the remote doc is deleted. +3. **Check `target_documents`**: Is the document explicitly linked to a TargetID that you thought was closed? \ No newline at end of file From 63b6467e43576a1e812673bfa22ec242fb25dc92 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Wed, 17 Dec 2025 23:06:31 +0000 Subject: [PATCH 20/31] docs: Add garbage collection documentation and update references --- .../firestore/devdocs/garbage-collection.md | 62 +++++++++++++++++++ packages/firestore/devdocs/overview.md | 3 +- .../firestore/devdocs/persistence-schema.md | 5 +- packages/firestore/devdocs/query-lifecycle.md | 25 +++----- 4 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 packages/firestore/devdocs/garbage-collection.md diff --git a/packages/firestore/devdocs/garbage-collection.md b/packages/firestore/devdocs/garbage-collection.md new file mode 100644 index 0000000000..a520371305 --- /dev/null +++ b/packages/firestore/devdocs/garbage-collection.md @@ -0,0 +1,62 @@ +# Garbage Collection (LRU) + +This document details how the SDK manages local cache size to prevent unbounded growth. It explains the distinction between Eager and LRU collection, the criteria for deletion, and the sequence-number-based algorithm used to identify old data. + +## Strategies: Eager vs. LRU + +The SDK employs two different strategies depending on the persistence mode: + +1. **Eager GC (Memory Persistence)**: + * Used when persistence is disabled (in-memory only). + * **Behavior**: When a query is stopped (`unsubscribe()`), the SDK immediately releases the reference to the data. If no other active query references those documents, they are deleted from memory instantly. + * **Pros/Cons**: extremely memory efficient, but offers no offline caching across app restarts. + +2. **LRU GC (Disk Persistence)**: + * Used when persistence is enabled (IndexedDB). + * **Behavior**: When a query is stopped, the data remains on disk. A background process periodically checks the total cache size. If it exceeds a threshold, the "Least Recently Used" data is purged. + * **Pros/Cons**: Supports offline apps and faster re-querying, but requires complex management of "Sequence Numbers" to track usage. + +*The rest of this document focuses on the LRU strategy.* + +## What is Collected? + +Garbage collection runs in the background. It does not indiscriminately delete data. It looks for **Eligible** items: + +### 1. Inactive Targets +A `Target` (internal query representation) is eligible for collection if it is no longer being listened to by the user. + +### 2. Orphaned Documents +A document is only eligible for collection if it is **Orphaned**. A document is Orphaned if: +* **No Active Targets**: It does not match *any* currently active query listener. +* **No Pending Mutations**: There are no local edits (Sets/Patches) waiting to be sent to the backend. + +> **Note**: Mutations are *never* garbage collected. They are only removed once the backend accepts or rejects them. + +## Key Concepts + +### Sequence Numbers (The Logical Clock) +To determine "recency," the SDK maintains a global `last_sequence_number` in the `target_globals` table. +* **Tick**: Every transaction (write, query listen, remote update) increments this number. +* **Tagging**: When a Target is actively listened to or updated, its `last_listen_sequence_number` is updated to the current global tick. +* **Effect**: Higher numbers = More recently used. + +### The Reference Map (`target_documents`) +This table acts as a reference counter linking Documents to Targets. +* **Active Association**: If `target_id: 2` matches `doc_key: A`, a row exists. +* **Sentinel Rows (`target_id: 0`)**: If a document exists in the cache but is not matched by *any* specific target (perhaps previously downloaded, or part of a target that was deleted), it may have a row with `target_id: 0`. This marks the document as present but potentially orphaned. + +## The Collection Algorithm + +The `LruGarbageCollector` runs periodically (e.g., every few minutes). + +1. **Threshold Check**: It calculates the byte size of the current cache. If `CurrentSize < CacheSizeBytes` (default 100MB), the process aborts. +2. **Calculate Cutoff**: + * The GC decides how many items to cull (e.g., 10%). + * It queries the `target_documents` table, ordered by `sequence_number` ASC. + * It finds the sequence number at the 10th percentile. This becomes the **Upper Bound**. +3. **Sweep Targets**: + * Any Target in the `targets` table with a `last_listen_sequence_number` <= **Upper Bound** is deleted. + * This removes the "Active" link for any documents associated with that target. +4. **Sweep Documents**: + * The GC scans for documents that have *no* rows in `target_documents` (or only sentinel rows) AND have a sequence number <= **Upper Bound**. + * These "Orphaned" documents are deleted from the `remote_documents` table. \ No newline at end of file diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index 07a1007884..c321e61aba 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -44,7 +44,8 @@ To navigate the internals of the SDK, use the following guide: * **[Query Lifecycle](./query-lifecycle.md)**: The state machine of a query. **Read this** to understand how querying and offline capabilities work. ### Subsystem Deep Dives -* **[Persistence Schema](./persistence-schema.md)**: A reference guide for the IndexedDB tables (e.g., `remote_documents`, `mutation_queues`). +* **[Persistence Schema](./persistence-schema.md)**: A reference guide for the IndexedDB tables. +* **[Garbage Collection](./garbage-collection.md)**: Details the LRU algorithm, Sequence Numbers, and how the SDK manages cache size. * **[Query Execution](./query-execution.md)**: Details on the algorithms used by the Local Store to execute queries (Index Scans vs. Full Collection Scans). * **[Bundles](./bundles.md)**: How the SDK loads and processes data bundles. diff --git a/packages/firestore/devdocs/persistence-schema.md b/packages/firestore/devdocs/persistence-schema.md index 53cb893632..83967ba79f 100644 --- a/packages/firestore/devdocs/persistence-schema.md +++ b/packages/firestore/devdocs/persistence-schema.md @@ -33,7 +33,10 @@ While the Android/iOS SDKs use SQLite, the JS SDK uses IndexedDB Object Stores. ### `target_documents` (The Index) * **Concept**: A reverse index mapping `TargetID` $\leftrightarrow$ `DocumentKey`. -* **Purpose**: Optimization. When a query is executed locally, the SDK uses this index to quickly identify which documents belong to a specific TargetID without scanning the entire `remote_documents` table. +* **Purpose**: + 1. **Query Execution**: Quickly identify documents for a query. + 2. **Garbage Collection**: Acts as a reference counter. If a document has entries here with active TargetIDs, it cannot be collected. +* **Sentinel Rows**: A row with `TargetID = 0` indicates the document exists in the cache but may not be attached to any active listener. These are primary candidates for Garbage Collection. * **Maintenance**: This is updated whenever a remote snapshot adds/removes a document from a query view. ## Metadata & Garbage Collection Stores diff --git a/packages/firestore/devdocs/query-lifecycle.md b/packages/firestore/devdocs/query-lifecycle.md index 056f8f5012..925aeeebc7 100644 --- a/packages/firestore/devdocs/query-lifecycle.md +++ b/packages/firestore/devdocs/query-lifecycle.md @@ -79,22 +79,15 @@ When a user calls `unsubscribe()`, data is **not** immediately deleted. ## Phase 6: Garbage Collection (The "Death" of a Query) -Since data persists after `unsubscribe()`, the SDK must actively manage disk usage. This is handled by the **LruGarbageCollector**. - -### Sequence Numbers -Every transaction (write, listen, update) increments a global `last_sequence_number` in the `target_globals` table. -* **Active Targets**: When a query is listened to, its `target` entry is updated with the current global sequence number. -* **Inactive Targets**: Old queries retain older sequence numbers. - -### The GC Process -When the SDK detects memory/disk pressure (or periodically): -1. **Reference Delegate**: Scans the `target_documents` reverse index. -2. **Orphan Check**: It identifies documents that belong *only* to Targets that are: - * Inactive (0 listeners). - * Old (Sequence number is below the GC threshold). -3. **Purge**: - * The document is deleted from `remote_documents`. - * The `Target` metadata is eventually removed from the `targets` table. +Since data persists after `unsubscribe()`, the SDK must actively manage disk usage. + +* **Eager GC**: If persistence is disabled, data is cleared from memory immediately when the listener count hits 0. +* **LRU GC**: If persistence is enabled, the data remains on disk for offline availability. + +The **LruGarbageCollector** runs periodically to keep the cache within the configured size (default 40MB/100MB). It uses a "Sequence Number" system to track when data was last used. + +For a detailed walkthrough of the algorithm, Sequence Numbers, and Orphaned Documents, see **[Garbage Collection](./garbage-collection.md)**. + ## Debugging Tips From 83bc62d28f1143094b0a7520d9fc93199194724d Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Wed, 17 Dec 2025 23:26:01 +0000 Subject: [PATCH 21/31] docs: Add write lifecycle documentation and update schema details --- packages/firestore/devdocs/overview.md | 1 + .../firestore/devdocs/persistence-schema.md | 24 +++++---- packages/firestore/devdocs/query-lifecycle.md | 4 +- packages/firestore/devdocs/write-lifecycle.md | 54 +++++++++++++++++++ 4 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 packages/firestore/devdocs/write-lifecycle.md diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index c321e61aba..ff5472e30c 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -42,6 +42,7 @@ To navigate the internals of the SDK, use the following guide: ### Core Concepts * **[Architecture](./architecture.md)**: The high-level block diagram of the system (API -> Core -> Local -> Remote). * **[Query Lifecycle](./query-lifecycle.md)**: The state machine of a query. **Read this** to understand how querying and offline capabilities work. +* **[Write Lifecycle](./write-lifecycle.md)**: How writes work (Mutations, Batches, Overlays). ### Subsystem Deep Dives * **[Persistence Schema](./persistence-schema.md)**: A reference guide for the IndexedDB tables. diff --git a/packages/firestore/devdocs/persistence-schema.md b/packages/firestore/devdocs/persistence-schema.md index 83967ba79f..4e41e8edb6 100644 --- a/packages/firestore/devdocs/persistence-schema.md +++ b/packages/firestore/devdocs/persistence-schema.md @@ -12,16 +12,20 @@ While the Android/iOS SDKs use SQLite, the JS SDK uses IndexedDB Object Stores. * **Value**: * **Document Data**: The serialized Protobuf of the document. * **ReadTime**: The snapshot version at which this document was read. -* **Note**: This store **never** contains local, unacknowledged writes. It only contains data confirmed by the server. To see what the developer sees, we overlay the `mutation_queues` on top of this. +* **Note**: This store **never** contains local, unacknowledged writes. It only contains data confirmed by the server. To see what the developer sees, we overlay the `mutation_queue` on top of this. -### `mutation_queues` -* **Concept**: The "Pending Writes" queue. -* **Key**: `BatchID` (Integer, auto-incrementing). -* **Grouping**: Queues are partitioned by **UID**. When a developer logs out, the SDK switches to a different queue. -* **Value**: - * **Mutation**: The serialized operation (Set, Patch, Delete). - * **Metadata**: Timestamp, offsets. -* **Behavior**: When the network is available, the `RemoteStore` reads from this queue to send write batches to the backend. Once acknowledged, entries are removed. +### `mutation_queue` +* **Concept**: The "Pending Writes" queue. An ordered log of all local writes that have not yet been acknowledged by the server. +* **Key**: `(user_id, batch_id)`. Segregating by User ID ensures that if a user logs out, their pending writes do not leak to the next user. +* **Value**: A serialized `MutationBatch` containing one or more mutations (Set, Patch, Delete). +* **Behavior**: This is the "Single Source of Truth" for local changes. If the app restarts, the SDK replays these mutations to rebuild the Overlays. When the network is available, the `RemoteStore` reads from this queue to send write batches to the backend. Once acknowledged, entries are removed. + +### `document_overlays` +* **Concept**: A cache of the *result* of applying pending mutations. +* **Key**: `(user_id, document_key)`. +* **Purpose**: Read Performance. Without this table, the SDK would have to read the Remote Document and re-apply every pending mutation from the queue every time a query ran. +* **Lifecycle**: Created immediately when a user writes. Deleted immediately when the backend acknowledges the write (or rejects it). +* **Priority**: When the `LocalStore` reads a document, it checks this table first. If an entry exists, it takes precedence over `remote_documents`. ### `targets` * **Concept**: Metadata about active and cached queries. @@ -54,5 +58,5 @@ While the Android/iOS SDKs use SQLite, the JS SDK uses IndexedDB Object Stores. ## Data Relationships -1. **The "View"**: To construct a document for the developer, the SDK reads `remote_documents[key]` and applies any mutations found in `mutation_queues` matching that key. +1. **The "View"**: To construct a document for the developer, the SDK reads `remote_documents[key]` and applies any mutations found in `mutation_queue` matching that key. 2. **Garbage Collection**: The `LruGarbageCollector` uses `target_globals.last_sequence_number` and `targets.last_sequence_number` to determine which targets are old and can be evicted. It then uses `target_documents` to find which documents are no longer referenced by *any* target and deletes them from `remote_documents`. \ No newline at end of file diff --git a/packages/firestore/devdocs/query-lifecycle.md b/packages/firestore/devdocs/query-lifecycle.md index 925aeeebc7..167202be81 100644 --- a/packages/firestore/devdocs/query-lifecycle.md +++ b/packages/firestore/devdocs/query-lifecycle.md @@ -41,7 +41,7 @@ When a query runs (initially or after a remote update), the **Local Store** perf * *Deep Dive*: For details on Index Scans, Full Scans, and Optimization strategies, see [Query Execution & Indexing](./query-execution.md). 2. **Base State**: The store retrieves the confirmed server state from the `remote_documents` table. 3. **Overlay Application**: - * The store checks the `mutation_queues` for any pending writes associated with these keys. + * The store checks the `mutation_queue` for any pending writes associated with these keys. * These mutations are converted into **Overlays**. * The Overlay is applied on top of the Remote Document. 4. **Projection**: The final composed documents are sent to the Event Manager. @@ -94,5 +94,5 @@ For a detailed walkthrough of the algorithm, Sequence Numbers, and Orphaned Docu If you are debugging a **"Zombie Document"** (data appearing that should be gone) or **"Missing Data"**: 1. **Check `targets`**: Is there an active target (valid `resumeToken`) covering that document? -2. **Check `mutation_queues`**: Is there a pending mutation (BatchID) that hasn't been acknowledged? This creates an Overlay that persists even if the remote doc is deleted. +2. **Check `mutation_queue`**: Is there a pending mutation (BatchID) that hasn't been acknowledged? This creates an Overlay that persists even if the remote doc is deleted. 3. **Check `target_documents`**: Is the document explicitly linked to a TargetID that you thought was closed? \ No newline at end of file diff --git a/packages/firestore/devdocs/write-lifecycle.md b/packages/firestore/devdocs/write-lifecycle.md new file mode 100644 index 0000000000..a86c83d87d --- /dev/null +++ b/packages/firestore/devdocs/write-lifecycle.md @@ -0,0 +1,54 @@ +# Write Lifecycle & Latency Compensation + +This document details the lifecycle of a write operation (Set, Update, Delete) from the moment the API is called to when it is committed to the backend. It focuses on **Mutations**, **Overlays**, and how the SDK achieves instant **Latency Compensation**. + +## Key Concepts + +* **Mutation**: An operation that modifies a document (e.g., `SetMutation`, `PatchMutation`, `DeleteMutation`). +* **Mutation Batch**: A group of mutations that must be applied atomically. Every user write creates a new Batch with a unique `BatchID`. +* **Overlay**: A "Materialized View" of the changes applied to a document. Instead of re-calculating the result of a mutation every time a query runs, the SDK calculates the result *once* at write time and saves it as an Overlay. + +## Phase 1: Mutation Creation & Batching + +When a user calls `setDoc` or `updateDoc`: +1. **Validation**: The SDK validates the data locally. +2. **Batching**: The operation is wrapped in a `MutationBatch`. +3. **Persistence**: The batch is serialized and saved to the `mutation_queue` table in IndexedDB. + * **Partitioning**: Queues are partitioned by User ID. If the user is offline, these batches accumulate in the queue. + +## Phase 2: Overlay Calculation (Optimization) + +To ensure queries run fast, the SDK does not apply raw mutations to remote documents during every query execution. Instead, it pre-calculates the result. + +1. **Base State**: The SDK retrieves the current state of the document (from `remote_documents`). +2. **Apply**: It applies the new `Mutation` to the base state to determine what the document *should* look like locally. +3. **Persist Overlay**: This resulting state is saved to the `document_overlays` table. + * **Field Mask**: The overlay tracks specifically which fields were modified. +4. **Latency Compensation**: The `Event Manager` immediately triggers active listeners. The listeners read the `Overlay` instead of the `Remote Document`, giving the user the illusion of instant updates. + +> **Formula:** `Local View = Remote Document + Overlay` + +## Phase 3: Synchronization (The Write Pipeline) + +The `SyncEngine` manages the flow of data to the server: + +1. **Filling the Pipeline**: The `RemoteStore` reads the `mutation_queue` in order of `BatchID` (FIFO). +2. **Transmission**: Mutations are sent to the backend via gRPC (or REST in Lite). +3. **Atomicity**: If a batch contains multiple writes, the backend guarantees they are applied together or not at all. + +## Phase 4: Acknowledgement & Cleanup + +When the backend responds: + +### Scenario A: Success (Ack) +1. **Commit**: The backend commits the change and returns the authoritative version of the document (and transformation results, like server timestamps). +2. **Update Remote**: The SDK updates the `remote_documents` table with this new server version. +3. **Cleanup**: + * The `MutationBatch` is removed from `mutation_queue`. + * The corresponding `Overlay` is removed from `document_overlays` (since the Remote Document now matches the desired state). +4. **Re-Evaluation**: Active queries are re-run. Since the Overlay is gone, they now read the updated Remote Document. + +### Scenario B: Rejection +1. The `MutationBatch` is removed. +2. The `Overlay` is removed. +3. The Local View reverts to the `Remote Document` state (rolling back the optimistic update). \ No newline at end of file From d16ca0b94ef4dcedb0f7db5d02b10c7d0dd36e7e Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Thu, 18 Dec 2025 22:26:08 +0000 Subject: [PATCH 22/31] docs(firestore): add transaction lifecycle documentation Create a new transactions.md document detailing the implementation of optimistic concurrency control, retries, and the online-only write pipeline for transactions. Update architecture.md and overview.md to reference the new documentation. --- packages/firestore/devdocs/architecture.md | 7 ++- packages/firestore/devdocs/overview.md | 1 + packages/firestore/devdocs/transactions.md | 69 ++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 packages/firestore/devdocs/transactions.md diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index bd73cc0142..a5797d586c 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -60,8 +60,11 @@ All data modifications—creates, updates, and deletes—are treated as "writes. * **One-Time Writes**: When a user performs a write (create, update, or delete), the operation is not sent directly to the backend. Instead, it's treated as a "mutation" and added to the local **Mutation Queue**. The SDK "optimistically" assumes the write will succeed on the backend and immediately reflects the change in the local view of the data, making the change visible to local queries. The SDK then works to synchronize this queue with the backend. This design is crucial for supporting offline functionality, as pending writes can be retried automatically when network connectivity is restored. -* **Transactions**: For grouping multiple write operations into a single atomic unit, the SDK provides `runTransaction`. Unlike standard writes, transactions do not use the optimistic, offline-capable write pipeline. Instead, they are sent directly to the backend, which requires an active internet connection. This ensures atomicity but means transactions do not benefit from the offline capabilities of the standard write pipeline. - +* **Transactions**: For grouping multiple write operations into a single atomic unit, the SDK provides `runTransaction`. Unlike standard writes, transactions do not use the optimistic, offline-capable write pipeline (Mutation Queue). Instead, they use an **Optimistic Concurrency Control** mechanism dependent on the backend. + * They are **Online-only**: Reads and writes communicate directly with the backend via RPCs. + * They are **Atomic**: All operations succeed or fail together. + * They are **Retriable**: The SDK automatically retries the transaction if the underlying data changes on the server during execution. + * For implementation details, see [Transactions](./transactions.md). # Data Flow diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index ff5472e30c..bb7da7d0f7 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -48,6 +48,7 @@ To navigate the internals of the SDK, use the following guide: * **[Persistence Schema](./persistence-schema.md)**: A reference guide for the IndexedDB tables. * **[Garbage Collection](./garbage-collection.md)**: Details the LRU algorithm, Sequence Numbers, and how the SDK manages cache size. * **[Query Execution](./query-execution.md)**: Details on the algorithms used by the Local Store to execute queries (Index Scans vs. Full Collection Scans). +* **[Transactions](./transactions.md)**: How the SDK implements Optimistic Concurrency Control, retries, and the online-only write pipeline. * **[Bundles](./bundles.md)**: How the SDK loads and processes data bundles. ### Developer Guides diff --git a/packages/firestore/devdocs/transactions.md b/packages/firestore/devdocs/transactions.md new file mode 100644 index 0000000000..74c3cdd6c4 --- /dev/null +++ b/packages/firestore/devdocs/transactions.md @@ -0,0 +1,69 @@ +# Transaction Lifecycle & Mechanics + +This document details the internal implementation of Transactions in the Firestore JavaScript SDK. Unlike standard writes, which use the [Write Lifecycle](./write-lifecycle.md) (Mutation Queue, Overlays, Sync Engine), Transactions are **online-only** operations that communicate directly with the backend. + +## Optimistic vs. Pessimistic Concurrency + +It is critical to distinguish how the Client-Side SDKs (JS, Android, iOS) handle transactions versus the Server-Side SDKs (Node.js, Java, Go, etc.). + +* **Server SDKs (Pessimistic Locking)**: + 1. Call `BeginTransaction` RPC. Backend returns a `TransactionID`. + 2. Reads hold a **Lock** on the documents on the server. + 3. Other clients cannot modify these documents until the transaction commits or times out. + 4. Writes are committed using the `TransactionID`. + +* **JavaScript / Mobile SDKs (Optimistic Concurrency)**: + 1. **No Locks**: The SDK reads documents without acquiring a server-side lock. + 2. **Preconditions**: When committing, the SDK sends the writes along with a **Precondition** (usually the `updateTime` of the document version that was read). + 3. **Verification**: The backend verifies that the documents have not changed since they were read. If they have, the transaction fails. + 4. **Retry**: The SDK automatically retries the transaction function (up to 5 times) with exponential backoff. + +### Why Optimistic? +Mobile and Web clients have unreliable network connectivity. If a client acquired a Pessimistic Lock and then lost connectivity, that lock would block all other writers to that document until it timed out. Optimistic concurrency ensures that a single flaky client cannot paralyze the system for others. + +## The Transaction Lifecycle + +Because transactions bypass the `LocalStore` and `SyncEngine`, they do not persist data to IndexedDB during execution. + +### 1. Execution +The `runTransaction` function accepts an `updateFunction`. This function is executed repeatedly until success or a non-retriable error occurs. + +### 2. Reads (`get`) +When a user reads a document inside a transaction: +* **Bypasses Cache**: The SDK does *not* look in `remote_documents` or `mutation_queue`. It forces a network fetch. +* **RPC**: It uses the `BatchGetDocuments` RPC. +* **No Transaction ID**: Unlike server SDKs, the `BatchGetDocuments` request does **not** include a Transaction ID. It is a standard read. +* **Versioning**: The SDK records the `updateTime` and `key` of every document read. These will be used later for verification. + +### 3. Writes (`set`, `update`, `delete`) +Writes are not sent to the network immediately. They are buffered in memory within the `Transaction` object. +* **Validation**: The SDK enforces the rule that **all reads must occur before any writes**. Once a write is buffered, the SDK throws an error if a subsequent read is attempted. + +### 4. Commit +When the `updateFunction` completes successfully, the SDK attempts to commit. +* **RPC**: It uses the `Commit` RPC (a single atomic batch). +* **Preconditions**: For every document that was read and is now being written to (or verified), the SDK attaches a `Precondition`. + * *Example*: "Only update `doc/A` if its `updateTime` is still `Timestamp(123)`." +* **Verify Mutations**: If a document was read but *not* modified, the SDK still needs to ensure it didn't change (as it might have influenced the business logic). The SDK adds a specific `VerifyMutation` to the commit batch, which performs a precondition check without modifying data. + +### 5. Backend Response +* **Success**: The backend applies all writes atomically. The SDK returns the result to the user. +* **Failure (Precondition Failed)**: This indicates contention (another client modified a document). The SDK captures this specific error code. +* **Retry**: If the error is retriable (Precondition Failed, Aborted), the SDK waits for a backoff period and then **re-runs the `updateFunction` from the start**. This requires re-fetching the fresh documents. + +## Architectural Bypass + +Transactions utilize a dedicated pathway in the `RemoteStore`/`Datastore` layer. + +1. **API Layer**: `runTransaction` is called. +2. **Core**: `TransactionRunner` manages the retry loop and backoff. +3. **Remote Store**: + * Standard queries use `WatchStream` (long-lived connection). + * Standard writes use `CommitStream` (requires `mutation_queue` persistence). + * **Transactions** use direct Unary RPCs (`BatchGetDocuments` and `Commit`) via the underlying `Datastore` helper. + +## Constraints + +* **Online Only**: Because they bypass the local cache and require server-side verification, transactions require an active network connection. They will fail if the client is offline. +* **Read-Your-Writes**: Within the scope of the transaction function, the SDK does not update the local cache with the pending writes. However, the transaction object tracks local changes to ensure that if you write to `DocA` and then read `DocA` (which is illegal in the public API, but conceptually relevant), you would see the change. +* **5 Retry Limit**: To prevent infinite loops during high contention, the SDK caps retries at 5 attempts. \ No newline at end of file From d485ae73af7fa1339afdc99ef164c3ee4065a7b5 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Thu, 18 Dec 2025 22:34:39 +0000 Subject: [PATCH 23/31] docs(firestore): add limbo resolution documentation Create limbo-resolution.md to detail how the SDK handles offline drift, existence filters, and bloom filters. Update overview.md and query-lifecycle.md to link to the new guide. --- .../firestore/devdocs/limbo-resolution.md | 51 +++++++++++++++++++ packages/firestore/devdocs/overview.md | 1 + packages/firestore/devdocs/query-lifecycle.md | 15 +++--- 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 packages/firestore/devdocs/limbo-resolution.md diff --git a/packages/firestore/devdocs/limbo-resolution.md b/packages/firestore/devdocs/limbo-resolution.md new file mode 100644 index 0000000000..3cdaafc7b8 --- /dev/null +++ b/packages/firestore/devdocs/limbo-resolution.md @@ -0,0 +1,51 @@ +# Limbo Resolution & Consistency + +This document details how the Firestore Client SDKs ensure the local cache remains consistent with the server after connectivity interruptions, specifically focusing on **Limbo Resolution**, **Resume Tokens**, and **Existence Filters**. + +## The Problem: Offline Drift + +When a client is online and listening to a query, the backend sends specific `Change` events (Added, Modified, Removed). The SDK keeps the local cache in sync by applying these deltas. + +However, when a client goes offline or disconnects: +1. **Drift Occurs**: Documents may be deleted or modified on the server such that they no longer match the query. +2. **Missing Negatives**: Upon reconnecting, the backend sends updates for documents that *changed*. However, if a document was deleted while the client was offline, the backend (using a Resume Token) might not send a specific "Delete" event because it doesn't track exactly which documents every offline client currently holds in their specific local cache. + +This creates a state where the local cache has "stale" documents that the server no longer considers part of the result set. + +## Mechanism 1: Resume Tokens (The Happy Path) + +To minimize bandwidth, the SDK and Backend use **Resume Tokens**. +* **Token**: An opaque binary blob (encoding a timestamp) received in every `ListenResponse`. +* **Resume**: When the SDK reconnects, it sends the last token it received. +* **Delta**: The backend sends only documents that have changed *since* that timestamp. +* **Gap**: This mechanism handles *adds* and *updates* perfectly. It struggles with *removes* if the server history is compacted or complex. + +## Mechanism 2: Existence Filters + +To detect inconsistency caused by the "Missing Negatives" problem, the backend includes an **Existence Filter** in the `ListenResponse`. + +* **The Count**: An Existence Filter is essentially a count of how many documents the server believes match the query. +* **The Check**: The SDK compares this count with the number of documents in its local cache for that specific target. +* **Mismatch**: If `LocalCount > ServerCount`, the SDK knows it is holding onto stale data. This triggers **Limbo Resolution**. + +## Mechanism 3: Bloom Filters (The Solution) + +Historically, upon detecting a mismatch, the SDK would drop the Resume Token and re-download the entire query result. This was expensive. The modern approach uses **Bloom Filters**. + +1. **Construction**: The backend constructs a Bloom Filter containing the IDs of all documents currently matching the query. +2. **Transmission**: This filter is sent to the client (probabilistic, highly compressed). +3. **Local Check**: The SDK checks every local document key against this Bloom Filter. + * **If NOT in filter**: The document is definitely gone from the server result set. + * **If IN filter**: The document *probably* exists (Bloom filters have false positives). + +## Limbo Resolution State Machine + +Documents that exist locally but failed the Bloom Filter check (or triggered a false positive check) enter **Limbo**. + +1. **Limbo State**: The document is present in the local cache, but the View cannot confirm if it is valid. We cannot simply delete it immediately because it might match *another* active query, or it might be a false positive from the Bloom Filter. +2. **Resolution Request**: The SDK spins up a dedicated, ephemeral `Watch` listener (effectively a `GetDocument` lookup) specifically for the key in Limbo. +3. **Outcome**: + * **Found**: The server confirms the document exists and sends the latest version. The SDK updates the cache. + * **Not Found**: The server returns "Not Found". The SDK deletes the document from the local cache (and removes it from the Query View). + +This process ensures that the client converges to a consistent state without re-downloading the entire result set, optimizing bandwidth usage. \ No newline at end of file diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index bb7da7d0f7..d9af226b15 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -49,6 +49,7 @@ To navigate the internals of the SDK, use the following guide: * **[Garbage Collection](./garbage-collection.md)**: Details the LRU algorithm, Sequence Numbers, and how the SDK manages cache size. * **[Query Execution](./query-execution.md)**: Details on the algorithms used by the Local Store to execute queries (Index Scans vs. Full Collection Scans). * **[Transactions](./transactions.md)**: How the SDK implements Optimistic Concurrency Control, retries, and the online-only write pipeline. +* **[Limbo Resolution](./limbo-resolution.md)**: How the SDK detects and cleans up stale data using Existence Filters and Bloom Filters. * **[Bundles](./bundles.md)**: How the SDK loads and processes data bundles. ### Developer Guides diff --git a/packages/firestore/devdocs/query-lifecycle.md b/packages/firestore/devdocs/query-lifecycle.md index 167202be81..942127f8f0 100644 --- a/packages/firestore/devdocs/query-lifecycle.md +++ b/packages/firestore/devdocs/query-lifecycle.md @@ -59,13 +59,16 @@ As the **Remote Store** receives snapshot events from the backend, the Sync Engi 4. If the view changes, Event Manager fires the user callback. ### The Limbo State -Limbo occurs when the local cache believes a document matches a query, but the server snapshot says otherwise (or is silent about it). +Limbo occurs when the local cache holds a document that the server implies should not be there (usually detected via an **Existence Filter Mismatch**). + +1. **Detection**: The Sync Engine compares the server's count of matching documents against the local count. If they disagree, it initiates the resolution process. +2. **Resolution**: The SDK uses **Bloom Filters** to identify exactly which local documents are stale. These documents enter "Limbo." +3. **Verification**: The Sync Engine spins up a targeted listener for the Limbo documents. + * If the server returns the document: It is updated. + * If the server returns "Not Found": It is removed from the view. + +*For a detailed explanation of Resume Tokens, Bloom Filters, and the mechanics of this process, see **[Limbo Resolution](./limbo-resolution.md)**.* -1. **Detection**: During snapshot processing, the Sync Engine notices a locally matching document is missing from the server's update. The document enters **Limbo**. -2. **Resolution**: The Sync Engine implicitly spins up a secondary "Limbo Resolution" listener specifically for that document's key. -3. **Verification**: - * If the server confirms the document exists: It is updated in `remote_documents` and stays in the view. - * If the server returns "Not Found" or the document no longer matches: It is removed from the local cache and the view. ## Phase 5: Teardown (Stop Listening) From d3f8973f9547d774010da7eef97fd93db4598043 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Thu, 18 Dec 2025 22:57:05 +0000 Subject: [PATCH 24/31] docs(firestore): add spec testing documentation --- packages/firestore/devdocs/overview.md | 1 + packages/firestore/devdocs/spec-tests.md | 69 ++++++++++++++++++++++++ packages/firestore/devdocs/testing.md | 51 +++++++++++++----- 3 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 packages/firestore/devdocs/spec-tests.md diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index d9af226b15..cecba13d3a 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -56,4 +56,5 @@ To navigate the internals of the SDK, use the following guide: * **[Code Layout](./code-layout.md)**: Maps the architectural components to specific source files and directories. * **[Build Process](./build.md)**: How to build the artifacts. * **[Testing](./testing.md)**: How to run unit and integration tests. +* **[Spec Tests](./spec-tests.md)**: Deep dive into the cross-platform JSON test suite. * **[Contributing](../CONTRIBUTING.md)**: How to run unit and integration tests. \ No newline at end of file diff --git a/packages/firestore/devdocs/spec-tests.md b/packages/firestore/devdocs/spec-tests.md new file mode 100644 index 0000000000..8c66e2c112 --- /dev/null +++ b/packages/firestore/devdocs/spec-tests.md @@ -0,0 +1,69 @@ +# Spec Tests (Cross-Platform Verification) + +Spec Tests are the backbone of the Firestore SDK's correctness strategy. They are a suite of deterministic, data-driven tests that verify the behavior of the **Sync Engine**, **Local Store**, and **Event Manager** without connecting to a real backend. + +## The "Sandwich" Architecture + +Spec tests operate by mocking the edges of the SDK while running the core logic for real. + +1. **Mocked Top (API Layer)**: instead of user code calling `doc.set()`, the test runner injects "User Actions" directly into the Event Manager/Sync Engine. +2. **Real Middle (Core)**: The Sync Engine, Local Store, Query Engine, and Garbage Collector run exactly as they do in production. +3. **Mocked Bottom (Remote Store)**: The network layer (gRPC/REST) is mocked out. The test runner intercepts outgoing writes and injects incoming Watch Stream events (snapshots, acks). + +This allows us to simulate complex race conditions—such as a Write Acknowledgment arriving exactly when a Watch Snapshot is processing—that are impossible to reproduce reliably in a real integration test. + +## Cross-Platform Consistency + +A unique feature of the Firestore SDKs is that they share logic tests across platforms (JavaScript, Android, iOS). + +* **Source of Truth**: The tests are written in **TypeScript** within the JavaScript SDK repository. +* **Compilation**: A build script runs the TS tests in a special mode that exports the steps and expectations into **JSON files**. +* **Consumption**: The Android and iOS SDKs ingest these JSON files and run them using their own platform-specific Test Runners. + +This ensures that if a query behaves a certain way on the Web, it behaves *exactly* the same on an iPhone or Android device. + +## Anatomy of a Spec Test + +A spec test consists of a sequence of **Steps**. Each step performs an action or asserts a state. + +```typescript +specTest('Local writes are visible immediately', [], () => { + // Step 1: User sets a document + userSets('collection/key', { foo: 'bar' }); + + // Step 2: Expect an event (Optimistic update) + expectEvents({ + acknowledgedDocs: ['collection/key'], + events: [{ type: 'added', doc: 'collection/key' }] + }); + + // Step 3: Network acknowledges the write + writeAcks(1); // Ack version 1 + + // Step 4: Watch stream sends the confirmed data + watchSends({ affects: ['collection/key'] }, ...); +}); + +### Key Helpers +* `userListens(query)`: Simulates a user calling `onSnapshot`. +* `userSets(key, val)`: Simulates a user write. +* `watchSends(snapshot)`: Simulates the backend sending data over the Watch stream. +* `expectEvents(events)`: Asserts that the Event Manager emitted specific snapshots to the user. + +## Configuration & Tags + +Spec tests can be configured to run only in specific environments using **Tags**. + +* `multi-client`: Runs the test in a multi-tab environment (simulating IndexedDB concurrency). +* `eager-gc`: Runs only when Garbage Collection is set to Eager (Memory persistence). +* `durable-persistence`: Runs only when using IndexedDB/SQLite. +* `exclusive`: **Debug Tool**. If you add this tag to a test, the runner will skip all other tests and only run this one. This is critical for debugging because the sheer volume of spec tests makes logs unreadable otherwise. + +## Debugging Spec Tests + +Debugging spec tests can be challenging because the code you are stepping through is often the *Test Runner* interpreting the JSON, rather than the test logic itself. + +1. **Use `exclusive`**: Always isolate the failing test. +2. **Trace the Helper**: If `userListens` fails, set a breakpoint in the `spec_test_runner.ts` implementation of that step to see how it interacts with the Sync Engine. +3. **Check Persistence**: Remember that spec tests usually run twice: once with Memory Persistence and once with IndexedDB. A failure might only happen in one mode. + diff --git a/packages/firestore/devdocs/testing.md b/packages/firestore/devdocs/testing.md index 235dde9a43..f678208694 100644 --- a/packages/firestore/devdocs/testing.md +++ b/packages/firestore/devdocs/testing.md @@ -5,16 +5,43 @@ This document provides a detailed explanation of the Firestore JavaScript SDK te # Tech Stack - karma, mocha, chai -# Strategy -- Firebase emulator for local development -- Integration testing with the backend +# Testing Strategy + +The Firestore JS SDK employs a three-tiered testing strategy to ensure reliability, correctness, and cross-platform consistency. + +## 1. Unit Tests +* **Scope**: Individual classes and functions. +* **Location**: Co-located with source files (e.g., `src/core/query.test.ts`). +* **Purpose**: Validating low-level logic, util functions, and individual component behavior in isolation. + +## 2. Spec Tests (The Core Logic) +* **Scope**: The interaction between the Sync Engine, Local Store, and Event Manager. +* **Location**: `src/core/test/spec_test.ts` and `src/specs/*.ts`. +* **Purpose**: Validating the complex state machine of Firestore without the flakiness of a real network. These tests mock the network layer to simulate specific protocol events (e.g., Watch stream updates, write handshakes). +* **Cross-Platform**: These tests are exported as JSON and run by the Android and iOS SDKs to ensure consistent behavior across all platforms. +* **Deep Dive**: See **[Spec Tests](./spec-tests.md)** for details on how to write and debug these. + +## 3. Integration Tests +* **Scope**: End-to-End verification against a real Firestore backend (prod or emulator). +* **Location**: `test/integration/`. +* **Purpose**: Verifying that the client protocol actually matches what the real backend server expects. +* **Behavior**: These tests create real writes and listeners. They are slower and subject to network timing, but essential for catching protocol drifts. + +## Running Tests + +### Unit & Spec Tests +Run via Karma. +```bash +yarn test +``` + +### Integration Tests +Requires the Firebase Emulator Suite running. +```bash +# Start emulators +yarn emulators:start + +# In another terminal, run integration tests +yarn test:integration +``` -## Component Testing -* **Query Engine Tests**: We rely on specific spec tests to ensure the Query Engine picks the correct strategy (e.g., verifying that it uses an Index when available rather than scanning). -* **Integration Tests**: Use the Firebase Emulator to verify that "Limbo Resolution" correctly fetches documents when the local cache drifts from the server state. - - -# Patterns and Practices - - -# Spec Tests \ No newline at end of file From 9892f5791f045eaa01698045dd7357b0f308e335 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 19 Dec 2025 00:37:28 +0000 Subject: [PATCH 25/31] docs(firestore): Improving discoverability and giving more product context --- packages/firestore/CONTRIBUTING.md | 2 ++ packages/firestore/README.md | 4 ++++ packages/firestore/devdocs/architecture.md | 1 + packages/firestore/devdocs/build.md | 4 +++- packages/firestore/devdocs/overview.md | 18 +++++++++++++++--- packages/firestore/devdocs/prerequisites.md | 2 +- packages/firestore/devdocs/testing.md | 20 ++------------------ 7 files changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/firestore/CONTRIBUTING.md b/packages/firestore/CONTRIBUTING.md index 49780249ea..79f26f2684 100644 --- a/packages/firestore/CONTRIBUTING.md +++ b/packages/firestore/CONTRIBUTING.md @@ -5,6 +5,8 @@ contributing to the Firebase JavaScript SDK (including Cloud Firestore). Follow instructions there to install dependencies, build the SDK, and set up the testing environment. +For a deep dive into the testing strategy and architecture, see [Testing Strategy](devdocs/testing.md). + ## Integration Testing ### Setting up a project for testing diff --git a/packages/firestore/README.md b/packages/firestore/README.md index 504f6ab2b6..ac7017d289 100644 --- a/packages/firestore/README.md +++ b/packages/firestore/README.md @@ -17,6 +17,10 @@ Docs][reference-docs]. [reference-docs]: https://firebase.google.com/docs/reference/js/ +## Internal Documentation + +If you are a contributor or maintainer, please see the [Internal Developer Documentation](./devdocs/overview.md). + ## Contributing See [Contributing to the Firebase SDK](../../CONTRIBUTING.md) for general information about contributing to the firebase-js-sdk repo and diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index a5797d586c..6408927446 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -12,6 +12,7 @@ The SDK is composed of several key components that work together to provide the * **Core**: * **Event Manager**: Acts as a central hub for all eventing in the SDK. It is responsible for routing events between the API Layer and Sync Engine. It manages query listeners and is responsible for raising snapshot events, as well as handling connectivity changes and some query failures. * **Sync Engine**: The central controller of the SDK. It acts as the glue between the Event Manager, Local Store, and Remote Store. + * **Target**: The backend protocol's internal representation of a recurring Query. While a `Query` is a user-intent (e.g., "users where age > 18"), a `Target` is the allocated stream ID (`TargetID`) that the Watch implementation uses to track that query's state over the network. The **Coordinator** maps ephemeral user Queries to stable system Targets. * **Coordinator**: It bridges the **User World** (Query) and **System World** (Target), converting public API calls into internal `TargetIDs`. * **View Construction**: It manages the user-facing view using the formula: `View = Remote Document + Overlay`. * **Remote Document**: The authoritative state from the backend. diff --git a/packages/firestore/devdocs/build.md b/packages/firestore/devdocs/build.md index 55b5e7b395..d002debddd 100644 --- a/packages/firestore/devdocs/build.md +++ b/packages/firestore/devdocs/build.md @@ -1,3 +1,5 @@ # Build Process -This document provides a detailed explanation of the Firestore JavaScript SDK build process for the main and lite packages. +> **Note**: This documentation is currently under construction and will be updated soon. + +For current build instructions, test commands, and setup, please go to [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index cecba13d3a..a5da6df67b 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -16,11 +16,21 @@ The primary goals of this SDK are: * Offer a lightweight version for applications that do not require advanced features. * Maintain API and architectural symmetry with the [Firestore Android SDK](https://github.com/firebase/firebase-android-sdk) and [Firestore iOS SDK](https://github.com/firebase/firebase-ios-sdk). This consistency simplifies maintenance and makes it easier to port features between platforms. The public API is intentionally consistent across platforms, even if it means being less idiomatic, to allow developers to more easily port their application code. +## Designed for Flicker-Free Responsiveness + +Firestore is designed to help developers build responsive front-end applications that eliminate UI flicker. + +1. **Immediate Cache Results**: The SDK returns query results from the local cache immediately, while fetching the latest data from the server in the background. +2. **Optimistic Updates**: Writes are applied to the local cache *instantly*, allowing the UI to update without waiting for network confirmation. +3. **Background Synchronization**: The SDK handles the network communication to commit these changes to the backend asynchronously. + +*This means the "Happy Path" handles latency automatically. You don't write special code to manage loading states for every interaction; the SDK provides instant feedback by default.* + ## Key Concepts & Vocabulary * **Query**: The client-side representation of a data request (filters, order bys). -* **Target**: The backend's representation of a Query. The SDK allocates a unique integer `TargetID` for every unique query to manage the real-time stream. + * **Mutation**: A user-initiated change (Set, Update, Delete). Mutations are queued locally and sent to the backend. * **Overlay**: The computed result of applying a Mutation to a Document. We store these to show "Optimistic Updates" instantly without modifying the underlying "Remote Document" until the server confirms the write. * **Limbo**: A state where a document exists locally and matches a query, but the server hasn't explicitly confirmed it belongs to the current snapshot version. The SDK must perform "Limbo Resolution" to ensure these documents are valid. @@ -39,6 +49,9 @@ The Firestore JavaScript SDK is divided into two main packages: To navigate the internals of the SDK, use the following guide: +### Getting Started (Build & Run) +* **[Start Here: Build & Run](../CONTRIBUTING.md)**: How to set up the repo, build the SDK, and run tests. + ### Core Concepts * **[Architecture](./architecture.md)**: The high-level block diagram of the system (API -> Core -> Local -> Remote). * **[Query Lifecycle](./query-lifecycle.md)**: The state machine of a query. **Read this** to understand how querying and offline capabilities work. @@ -56,5 +69,4 @@ To navigate the internals of the SDK, use the following guide: * **[Code Layout](./code-layout.md)**: Maps the architectural components to specific source files and directories. * **[Build Process](./build.md)**: How to build the artifacts. * **[Testing](./testing.md)**: How to run unit and integration tests. -* **[Spec Tests](./spec-tests.md)**: Deep dive into the cross-platform JSON test suite. -* **[Contributing](../CONTRIBUTING.md)**: How to run unit and integration tests. \ No newline at end of file +* **[Spec Tests](./spec-tests.md)**: Deep dive into the cross-platform JSON test suite. \ No newline at end of file diff --git a/packages/firestore/devdocs/prerequisites.md b/packages/firestore/devdocs/prerequisites.md index dabe19d7b9..dc5f4962ae 100644 --- a/packages/firestore/devdocs/prerequisites.md +++ b/packages/firestore/devdocs/prerequisites.md @@ -23,7 +23,7 @@ Before contributing to this codebase, you should have a strong understanding of ### Domain Knowledge -* **[Google Cloud Firestore](https://firebase.google.com/docs/firestore):** A deep understanding of Firestore's data model (documents, collections, subcollections), query language, and security rules is fundamental. +* **[Google Cloud Firestore](https://firebase.google.com/docs/firestore):** A general understanding of Firestore's data model (documents, collections, subcollections), query language, and security rules is fundamental. * **Databases:** A general understanding of databases, including key-value stores and relational databases, is helpful for understanding Firestore's design and trade-offs. * **Modern Web Application Architecture:** Familiarity with modern web application architecture and also server-side rendering (SSR), is beneficial for understanding how the SDK is used in practice. * **[Firebase](https://firebase.google.com/docs):** Familiarity with the Firebase platform is required, especially Firebase Auth and Firebase Functions. diff --git a/packages/firestore/devdocs/testing.md b/packages/firestore/devdocs/testing.md index f678208694..3af4e7ac48 100644 --- a/packages/firestore/devdocs/testing.md +++ b/packages/firestore/devdocs/testing.md @@ -1,4 +1,4 @@ -# Build Process +# Testing Strategy This document provides a detailed explanation of the Firestore JavaScript SDK testing strategy, tech stack, and patterns and practices. @@ -27,21 +27,5 @@ The Firestore JS SDK employs a three-tiered testing strategy to ensure reliabili * **Purpose**: Verifying that the client protocol actually matches what the real backend server expects. * **Behavior**: These tests create real writes and listeners. They are slower and subject to network timing, but essential for catching protocol drifts. -## Running Tests - -### Unit & Spec Tests -Run via Karma. -```bash -yarn test -``` - -### Integration Tests -Requires the Firebase Emulator Suite running. -```bash -# Start emulators -yarn emulators:start - -# In another terminal, run integration tests -yarn test:integration -``` +> **Note**: For instructions on how to run these tests, see **[CONTRIBUTING.md](../CONTRIBUTING.md)**. From d883769623e8251c90e213ffe894408544090239 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 19 Dec 2025 16:33:03 +0000 Subject: [PATCH 26/31] docs(firestore): Adding WIP sections --- packages/firestore/devdocs/build.md | 2 +- packages/firestore/devdocs/multi-tab.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 packages/firestore/devdocs/multi-tab.md diff --git a/packages/firestore/devdocs/build.md b/packages/firestore/devdocs/build.md index d002debddd..14789a367a 100644 --- a/packages/firestore/devdocs/build.md +++ b/packages/firestore/devdocs/build.md @@ -1,5 +1,5 @@ # Build Process -> **Note**: This documentation is currently under construction and will be updated soon. +> **Note**: This documentation is currently under construction. For current build instructions, test commands, and setup, please go to [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/packages/firestore/devdocs/multi-tab.md b/packages/firestore/devdocs/multi-tab.md new file mode 100644 index 0000000000..4f78d6621e --- /dev/null +++ b/packages/firestore/devdocs/multi-tab.md @@ -0,0 +1,7 @@ +# This document outlines how the Firestore JS SDK works across multiple tabs in the browser. + +> **Note**: This documentation is currently under construction. + +Reminder to self on what to include: + - This feature is only relevant in the JS SDK. The mobile SDKs do not have a concept of tabs therefore do not need to coordinate. + - End users might have multiple tabs open to the same website which puts contention on IndexedDB and the backend. \ No newline at end of file From 7ffcbf2efb912793086b9940a752da1caa98570c Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 19 Dec 2025 16:52:24 +0000 Subject: [PATCH 27/31] docs(firestore): Adding GEMINI.md for docs --- packages/firestore/devdocs/GEMINI.md | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 packages/firestore/devdocs/GEMINI.md diff --git a/packages/firestore/devdocs/GEMINI.md b/packages/firestore/devdocs/GEMINI.md new file mode 100644 index 0000000000..85212f0f48 --- /dev/null +++ b/packages/firestore/devdocs/GEMINI.md @@ -0,0 +1,47 @@ +# Firestore JavaScript SDK Developer Documentation + +This folder contains the developer documentation for the Firestore JavaScript SDK. + +**Audience:** +1. **Maintainers**: Engineers working on the SDK internals. +2. **AI Agents**: Automated assistants reading this consistency to understand the codebase. + +**NOT Audience:** +- 3rd Party Developers (App Developers). They should use the [official Firebase documentation](https://firebase.google.com/docs/firestore). + +# Entry Point +Start at [./overview.md](./overview.md). + +# Content Guidelines + +## Principles +- **High-Level Focus**: Explain architecture and data flow. Avoid duplicating code. +- **Why > How**: Explain *why* a design choice was made. The code shows *how*. +- **Reference by Name**: Use exact component/interface names (e.g., `Persistence`, `EventManager`). + +## Terminology +- **Concepts First**: **Aggressively favor** high-level English concepts over code identifiers. Only drop down to code identifiers when absolutely necessary for precise mapping. + * *Good*: "The Mutation Queue stores pending writes." + * *Bad*: "The `mutationQueues` store contains `DbMutationBatch` objects." + * *Acceptable (Mapping)*: "The Mutation Queue (implemented as `mutationQueues` store)..." +- **Avoid Over-Specification**: Do not generally reference private/internal variable names unless documenting that specific module's internals. +- **Strict Casing**: When you *must* reference code, use the **exact casing** found in the codebase (e.g., `mutationQueues`). +- **No "Translations"**: Never convert code names into snake_case. Either use the English Concept ("Remote Documents") or the exact Code Name (`remoteDocuments`). + +## Diagramming +Use **Mermaid** for diagrams. +- Flowcharts for logic. +- Sequence diagrams for async protocols. +- Class diagrams for component relationships. + +# Style Guide +- **Syntax**: Markdown (GFM). +- **Voice**: Active voice ("The SDK does X"). +- **Tense**: Present tense ("The query executes..."). +- **Mood**: Imperative for instructions ("Run the build..."). +- **Conciseness**: Short sentences. Bullet points where possible. + +# Maintenance +- **Co-location**: Keep documentation close to the code it describes (linked via `Code Layout`). +- **Atomic Updates**: Update documentation in the *same PR* as the feature or fix. +- **Freshness**: If you see stale docs, fix them immediately. From fa76efc75b6016d65eff88f337507bd19238bbf3 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 19 Dec 2025 18:32:53 +0000 Subject: [PATCH 28/31] docs(firestore): fixing spelling mistake --- packages/firestore/devdocs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index 6408927446..45d00aafd4 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -42,7 +42,7 @@ For a more detailed explanation of the contents of each directory, see the [Code ## Overview of features -At a high level, all interactions with Firestore can be categorized as either reading or writing data. The SDK provides different mechanisms for these operations, each with distinct guarantees and performance characteristics. There is also a special case of writing data called tansactions detailed below. +At a high level, all interactions with Firestore can be categorized as either reading or writing data. The SDK provides different mechanisms for these operations, each with distinct guarantees and performance characteristics. There is also a special case of writing data called transactions detailed below. ### Read Operations From d1c22c6e1b005cb7dcc070837d96b787792b90ae Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 19 Dec 2025 18:33:49 +0000 Subject: [PATCH 29/31] docs(firestore): fixing translation mistakes and changing to concepts vs code --- .../firestore/devdocs/garbage-collection.md | 20 ++++---- packages/firestore/devdocs/overview.md | 4 +- .../firestore/devdocs/persistence-schema.md | 50 +++++++++++-------- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/firestore/devdocs/garbage-collection.md b/packages/firestore/devdocs/garbage-collection.md index a520371305..376915d0e5 100644 --- a/packages/firestore/devdocs/garbage-collection.md +++ b/packages/firestore/devdocs/garbage-collection.md @@ -35,15 +35,15 @@ A document is only eligible for collection if it is **Orphaned**. A document is ## Key Concepts ### Sequence Numbers (The Logical Clock) -To determine "recency," the SDK maintains a global `last_sequence_number` in the `target_globals` table. +To determine "recency," the SDK maintains a global `lastListenSequenceNumber` in the **Target Globals** store (`targetGlobal`). * **Tick**: Every transaction (write, query listen, remote update) increments this number. -* **Tagging**: When a Target is actively listened to or updated, its `last_listen_sequence_number` is updated to the current global tick. +* **Tagging**: When a Target is actively listened to or updated, its `lastListenSequenceNumber` is updated to the current global tick. * **Effect**: Higher numbers = More recently used. -### The Reference Map (`target_documents`) -This table acts as a reference counter linking Documents to Targets. -* **Active Association**: If `target_id: 2` matches `doc_key: A`, a row exists. -* **Sentinel Rows (`target_id: 0`)**: If a document exists in the cache but is not matched by *any* specific target (perhaps previously downloaded, or part of a target that was deleted), it may have a row with `target_id: 0`. This marks the document as present but potentially orphaned. +### The Reference Map (`targetDocuments`) +The **Target-Document Index** (`targetDocuments`) acts as a reference counter linking Documents to Targets. +* **Active Association**: If `targetId: 2` matches `documentKey: A`, a row exists. +* **Sentinel Rows (`targetId: 0`)**: If a document exists in the cache but is not matched by *any* specific target (perhaps previously downloaded, or part of a target that was deleted), it may have a row with `targetId: 0`. This marks the document as present but potentially orphaned. ## The Collection Algorithm @@ -52,11 +52,11 @@ The `LruGarbageCollector` runs periodically (e.g., every few minutes). 1. **Threshold Check**: It calculates the byte size of the current cache. If `CurrentSize < CacheSizeBytes` (default 100MB), the process aborts. 2. **Calculate Cutoff**: * The GC decides how many items to cull (e.g., 10%). - * It queries the `target_documents` table, ordered by `sequence_number` ASC. + * It queries the **Target-Document Index** (`targetDocuments`) table, ordered by `sequenceNumber` ASC. * It finds the sequence number at the 10th percentile. This becomes the **Upper Bound**. 3. **Sweep Targets**: - * Any Target in the `targets` table with a `last_listen_sequence_number` <= **Upper Bound** is deleted. + * Any Target in the **Targets** table (`targets`) with a `lastListenSequenceNumber` <= **Upper Bound** is deleted. * This removes the "Active" link for any documents associated with that target. 4. **Sweep Documents**: - * The GC scans for documents that have *no* rows in `target_documents` (or only sentinel rows) AND have a sequence number <= **Upper Bound**. - * These "Orphaned" documents are deleted from the `remote_documents` table. \ No newline at end of file + * The GC scans for documents that have *no* rows in **Target-Document Index** (or only sentinel rows) AND have a sequence number <= **Upper Bound**. + * These "Orphaned" documents are deleted from the **Remote Document Cache** (`remoteDocuments`). \ No newline at end of file diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index a5da6df67b..e79e556266 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -58,9 +58,9 @@ To navigate the internals of the SDK, use the following guide: * **[Write Lifecycle](./write-lifecycle.md)**: How writes work (Mutations, Batches, Overlays). ### Subsystem Deep Dives -* **[Persistence Schema](./persistence-schema.md)**: A reference guide for the IndexedDB tables. -* **[Garbage Collection](./garbage-collection.md)**: Details the LRU algorithm, Sequence Numbers, and how the SDK manages cache size. * **[Query Execution](./query-execution.md)**: Details on the algorithms used by the Local Store to execute queries (Index Scans vs. Full Collection Scans). +* **[Garbage Collection](./garbage-collection.md)**: Details the LRU algorithm, Sequence Numbers, and how the SDK manages cache size. +* **[Persistence Schema](./persistence-schema.md)**: A reference guide for the IndexedDB tables. * **[Transactions](./transactions.md)**: How the SDK implements Optimistic Concurrency Control, retries, and the online-only write pipeline. * **[Limbo Resolution](./limbo-resolution.md)**: How the SDK detects and cleans up stale data using Existence Filters and Bloom Filters. * **[Bundles](./bundles.md)**: How the SDK loads and processes data bundles. diff --git a/packages/firestore/devdocs/persistence-schema.md b/packages/firestore/devdocs/persistence-schema.md index 4e41e8edb6..d018f84ec0 100644 --- a/packages/firestore/devdocs/persistence-schema.md +++ b/packages/firestore/devdocs/persistence-schema.md @@ -6,57 +6,63 @@ While the Android/iOS SDKs use SQLite, the JS SDK uses IndexedDB Object Stores. ## Core Object Stores -### `remote_documents` +### Remote Document Cache * **Concept**: The client's cache of the backend's "Source of Truth." +* **Implementation**: Stored in `remoteDocuments` (legacy: `remoteDocumentsV14`). * **Key**: `DocumentKey` (Path to the document). * **Value**: * **Document Data**: The serialized Protobuf of the document. * **ReadTime**: The snapshot version at which this document was read. -* **Note**: This store **never** contains local, unacknowledged writes. It only contains data confirmed by the server. To see what the developer sees, we overlay the `mutation_queue` on top of this. +* **Note**: This store **never** contains local, unacknowledged writes. It only contains data confirmed by the server. To see what the developer sees, we overlay the **Mutation Queue** on top of this. -### `mutation_queue` +### Mutation Queue * **Concept**: The "Pending Writes" queue. An ordered log of all local writes that have not yet been acknowledged by the server. -* **Key**: `(user_id, batch_id)`. Segregating by User ID ensures that if a user logs out, their pending writes do not leak to the next user. -* **Value**: A serialized `MutationBatch` containing one or more mutations (Set, Patch, Delete). -* **Behavior**: This is the "Single Source of Truth" for local changes. If the app restarts, the SDK replays these mutations to rebuild the Overlays. When the network is available, the `RemoteStore` reads from this queue to send write batches to the backend. Once acknowledged, entries are removed. +* **Implementation**: Split across `mutationQueues` (User Metadata) and `mutations` (Batch Data). +* **Key**: `(userId, batchId)`. Segregating by User ID ensures that if a user logs out, their pending writes do not leak to the next user. +* **Value**: A serialized `DbMutationBatch` containing one or more mutations (Set, Patch, Delete). +* **Behavior**: This is the "Single Source of Truth" for local changes. If the app restarts, the SDK replays these mutations to rebuild the **Document Overlays**. When the network is available, the `RemoteStore` reads from this queue to send write batches to the backend. Once acknowledged, entries are removed. -### `document_overlays` +### Document Overlays * **Concept**: A cache of the *result* of applying pending mutations. -* **Key**: `(user_id, document_key)`. +* **Implementation**: Stored in `documentOverlays`. +* **Key**: `(userId, documentKey)`. * **Purpose**: Read Performance. Without this table, the SDK would have to read the Remote Document and re-apply every pending mutation from the queue every time a query ran. * **Lifecycle**: Created immediately when a user writes. Deleted immediately when the backend acknowledges the write (or rejects it). -* **Priority**: When the `LocalStore` reads a document, it checks this table first. If an entry exists, it takes precedence over `remote_documents`. +* **Priority**: When the `LocalStore` reads a document, it checks this table first. If an entry exists, it takes precedence over **Remote Document Cache**. -### `targets` +### Targets * **Concept**: Metadata about active and cached queries. -* **Key**: `TargetID` (Internal Integer allocated by `SyncEngine`). +* **Implementation**: Stored in `targets`. +* **Key**: `TargetId` (Internal Integer allocated by `SyncEngine`). * **Value**: * **Canonical ID**: A hash string representing the query (filters, sort order). Used for deduplication. * **Resume Token**: An opaque token from the backend used to resume a stream without re-downloading all data. * **Last Sequence Number**: Used for Garbage Collection (LRU). -### `target_documents` (The Index) -* **Concept**: A reverse index mapping `TargetID` $\leftrightarrow$ `DocumentKey`. +### Target-Document Index +* **Concept**: A reverse index mapping `TargetId` $\leftrightarrow$ `DocumentKey`. +* **Implementation**: Stored in `targetDocuments`. * **Purpose**: 1. **Query Execution**: Quickly identify documents for a query. - 2. **Garbage Collection**: Acts as a reference counter. If a document has entries here with active TargetIDs, it cannot be collected. -* **Sentinel Rows**: A row with `TargetID = 0` indicates the document exists in the cache but may not be attached to any active listener. These are primary candidates for Garbage Collection. + 2. **Garbage Collection**: Acts as a reference counter. If a document has entries here with active `TargetId`s, it cannot be collected. +* **Sentinel Rows**: A row with `TargetId = 0` indicates the document exists in the cache but may not be attached to any active listener. These are primary candidates for Garbage Collection. * **Maintenance**: This is updated whenever a remote snapshot adds/removes a document from a query view. ## Metadata & Garbage Collection Stores -### `target_globals` +### Target Globals * **Concept**: A singleton store for global system state. +* **Implementation**: Stored in `targetGlobal`. * **Key**: Fixed singleton key. * **Value**: - * **`last_sequence_number`**: A global integer counter incremented on every transaction. - * **`target_count`**: Number of targets currently tracked. + * **`lastListenSequenceNumber`**: A global integer counter incremented on every transaction. + * **`targetCount`**: Number of targets currently tracked. -### `remote_document_changes` (Ephemeral) +### Remote Document Changes (Ephemeral) * **Concept**: A temporary staging area used during `SyncEngine` processing. -* **Purpose**: Used to track read-time updates for documents during a remote event application before they are committed to the main `remote_documents` store. +* **Purpose**: Used to track read-time updates for documents during a remote event application before they are committed to the main **Remote Document Cache**. ## Data Relationships -1. **The "View"**: To construct a document for the developer, the SDK reads `remote_documents[key]` and applies any mutations found in `mutation_queue` matching that key. -2. **Garbage Collection**: The `LruGarbageCollector` uses `target_globals.last_sequence_number` and `targets.last_sequence_number` to determine which targets are old and can be evicted. It then uses `target_documents` to find which documents are no longer referenced by *any* target and deletes them from `remote_documents`. \ No newline at end of file +1. **The "View"**: To construct a document for the developer, the SDK reads **Remote Document Cache** and applies any mutations found in **Mutation Queue** matching that key. +2. **Garbage Collection**: The `LruGarbageCollector` uses `TargetGlobal.lastListenSequenceNumber` and `Target.lastListenSequenceNumber` to determine which targets are old and can be evicted. It then uses **Target-Document Index** to find which documents are no longer referenced by *any* target and deletes them from **Remote Document Cache**. \ No newline at end of file From b92bedab99bbd84679dc19422681c3856d51f89c Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 19 Dec 2025 18:47:48 +0000 Subject: [PATCH 30/31] docs(firestore): moving feature overview into overview.md --- packages/firestore/devdocs/architecture.md | 26 ---------------------- packages/firestore/devdocs/overview.md | 25 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index 45d00aafd4..1d835522b2 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -40,32 +40,6 @@ The architecture and systems within the SDK map closely to the directory structu For a more detailed explanation of the contents of each directory, see the [Code Layout](./code-layout.md) documentation. -## Overview of features - -At a high level, all interactions with Firestore can be categorized as either reading or writing data. The SDK provides different mechanisms for these operations, each with distinct guarantees and performance characteristics. There is also a special case of writing data called transactions detailed below. - - -### Read Operations - -There are two fundamental ways to read data from Firestore: - -* **One-Time Reads**: This is for fetching a snapshot of data at a specific moment. It's a simple request-response model. You ask for a document or the results of a query, and the server sends back the data as it exists at that instant. - -* **Real-Time Listeners**: This allows you to subscribe to a document or a query. The server first sends you the initial data and then continues to push updates to your client in real time as the data changes. This is the foundation of Firestore's real-time capabilities. - -When a query is executed, the SDK immediately returns data from the local cache, which includes any pending optimistic writes from the **Mutation Queue**. This provides a fast, responsive experience. At the same time, the SDK sends the query to the Firestore backend to fetch the latest version of the documents. When the fresh documents arrive from the backend, the SDK takes these server-authoritative documents and re-applies any pending mutations from the local queue on top of them. It then re-runs the original query against this newly merged data. If the documents still match the query's criteria, they are delivered to the query listener again. This is a common occurrence and means a listener could see an event for the same document twice: first with the cached, optimistic data, and a second time after the backend data is reconciled. - -### Write Operations - -All data modifications—creates, updates, and deletes—are treated as "writes." The SDK is designed to make writes atomic and resilient. There are two fundamental ways to write data to Firestore: - -* **One-Time Writes**: When a user performs a write (create, update, or delete), the operation is not sent directly to the backend. Instead, it's treated as a "mutation" and added to the local **Mutation Queue**. The SDK "optimistically" assumes the write will succeed on the backend and immediately reflects the change in the local view of the data, making the change visible to local queries. The SDK then works to synchronize this queue with the backend. This design is crucial for supporting offline functionality, as pending writes can be retried automatically when network connectivity is restored. - -* **Transactions**: For grouping multiple write operations into a single atomic unit, the SDK provides `runTransaction`. Unlike standard writes, transactions do not use the optimistic, offline-capable write pipeline (Mutation Queue). Instead, they use an **Optimistic Concurrency Control** mechanism dependent on the backend. - * They are **Online-only**: Reads and writes communicate directly with the backend via RPCs. - * They are **Atomic**: All operations succeed or fail together. - * They are **Retriable**: The SDK automatically retries the transaction if the underlying data changes on the server during execution. - * For implementation details, see [Transactions](./transactions.md). # Data Flow diff --git a/packages/firestore/devdocs/overview.md b/packages/firestore/devdocs/overview.md index e79e556266..d7e74a4b33 100644 --- a/packages/firestore/devdocs/overview.md +++ b/packages/firestore/devdocs/overview.md @@ -27,6 +27,31 @@ Firestore is designed to help developers build responsive front-end applications *This means the "Happy Path" handles latency automatically. You don't write special code to manage loading states for every interaction; the SDK provides instant feedback by default.* + +## Operational Modes + +At a high level, all interactions with Firestore can be categorized as either reading or writing data. The SDK provides different mechanisms for these operations, each with distinct guarantees and performance characteristics. + +### Read Operations + +There are two fundamental ways to read data from Firestore: + +* **One-Time Reads**: This is for fetching a snapshot of data at a specific moment. It's a simple request-response model. You ask for a document or the results of a query, and the server sends back the data as it exists at that instant. + +* **Real-Time Listeners**: This allows you to subscribe to a document or a query. The server first sends you the initial data and then continues to push updates to your client in real time as the data changes. This is the foundation of Firestore's real-time capabilities. + +### Write Operations + +All data modifications—creates, updates, and deletes—are treated as "writes." The SDK is designed to make writes atomic and resilient. There are two fundamental ways to write data to Firestore: + +* **One-Time Writes**: When a user performs a write (create, update, or delete), the operation is not sent directly to the backend. Instead, it's treated as a "mutation" and added to the local **Mutation Queue**. The SDK "optimistically" assumes the write will succeed on the backend and immediately reflects the change in the local view of the data, making the change visible to local queries. The SDK then works to synchronize this queue with the backend. This design is crucial for supporting offline functionality, as pending writes can be retried automatically when network connectivity is restored. + +* **Transactions**: For grouping multiple write operations into a single atomic unit, the SDK provides `runTransaction`. Unlike standard writes, transactions do not use the optimistic, offline-capable write pipeline (Mutation Queue). Instead, they use an **Optimistic Concurrency Control** mechanism dependent on the backend. + * They are **Online-only**: Reads and writes communicate directly with the backend via RPCs. + * They are **Atomic**: All operations succeed or fail together. + * They are **Retriable**: The SDK automatically retries the transaction if the underlying data changes on the server during execution. + * For implementation details, see [Transactions](./transactions.md). + ## Key Concepts & Vocabulary * **Query**: The client-side representation of a data request (filters, order bys). From 5ef55faf38f5bfd2c1cb7ce6090025806ed6d8a0 Mon Sep 17 00:00:00 2001 From: Rafi Khan Date: Fri, 19 Dec 2025 19:17:58 +0000 Subject: [PATCH 31/31] docs(firestore): Introducing "Watch" as a concept --- packages/firestore/devdocs/architecture.md | 5 ++- packages/firestore/devdocs/query-execution.md | 7 +++- packages/firestore/devdocs/transactions.md | 3 ++ packages/firestore/devdocs/watch.md | 41 +++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 packages/firestore/devdocs/watch.md diff --git a/packages/firestore/devdocs/architecture.md b/packages/firestore/devdocs/architecture.md index 1d835522b2..02d1702c23 100644 --- a/packages/firestore/devdocs/architecture.md +++ b/packages/firestore/devdocs/architecture.md @@ -26,7 +26,10 @@ The SDK is composed of several key components that work together to provide the * **Query Engine**: Determines the most efficient strategy (Index vs. Scan) to identify documents matching a query in the local cache. * **Overlays**: A performance-optimizing cache that stores the calculated effect of pending mutations from the Mutation Queue on documents. Instead of re-applying mutations every time a document is read, the SDK computes this "overlay" once and caches it, allowing the Local View to be constructed more efficiently. * For a detailed breakdown of the IndexedDB structure and tables, see [Persistence Schema](./persistence-schema.md). -* **Remote Store**: The component responsible for all network communication with the Firestore backend. It manages the gRPC streams for reading and writing data, and it abstracts away the complexities of the network protocol from the rest of the SDK. +* **Remote Store**: The component responsible for all network communication with the Firestore backend. + * It manages the **Watch Stream** (see **[The Watch System](./watch.md)**) for reading and listening to data. + * It manages the gRPC streams for writing data. + * It abstracts away the complexities of the network protocol from the rest of the SDK. * **Persistence Layer**: The underlying storage mechanism used by the Local Store to persist data on the client. In the browser, this is implemented using IndexedDB. The architecture and systems within the SDK map closely to the directory structure, which helps developers navigate the codebase. Here is a mapping of the core components to their corresponding directories. diff --git a/packages/firestore/devdocs/query-execution.md b/packages/firestore/devdocs/query-execution.md index c28bedaf7f..9fd1cef364 100644 --- a/packages/firestore/devdocs/query-execution.md +++ b/packages/firestore/devdocs/query-execution.md @@ -32,7 +32,12 @@ To support efficient querying without blocking the main thread, the SDK utilizes ## Composite Queries (OR / IN) -Queries using `OR` or `IN` are not executed as a single monolithic scan. The SDK transforms these into **Disjunctive Normal Form (DNF)**—essentially breaking them into multiple sub-queries. +Queries using `OR` or `IN` are not executed as a single monolithic scan. + +> [!NOTE] +> **Scalability & Watch**: While functionality exists to run these queries against the backend, the SDK implements **Disjunctive Normal Form (DNF)** transformation primarily to enable efficient **local** execution using simpler indexes (as seen in `IndexedDbIndexManager`). This allows the SDK to support complex queries offline or against the cache without requiring full table scans. [See Watch System](./watch.md) for more on the backend interaction. + +The SDK transforms these into **Disjunctive Normal Form (DNF)**—essentially breaking them into multiple sub-queries. * **Execution**: Each sub-query is executed independently using the strategies above (Index vs. Scan). * **Union**: The resulting sets of document keys are unioned together in memory to produce the final result. \ No newline at end of file diff --git a/packages/firestore/devdocs/transactions.md b/packages/firestore/devdocs/transactions.md index 74c3cdd6c4..102321e072 100644 --- a/packages/firestore/devdocs/transactions.md +++ b/packages/firestore/devdocs/transactions.md @@ -62,6 +62,9 @@ Transactions utilize a dedicated pathway in the `RemoteStore`/`Datastore` layer. * Standard writes use `CommitStream` (requires `mutation_queue` persistence). * **Transactions** use direct Unary RPCs (`BatchGetDocuments` and `Commit`) via the underlying `Datastore` helper. +> [!WARNING] +> **Consistency Warning**: Transactions use a different endpoint (`runTransaction`) than the standard `Listen` (Watch) system. As a result, they **do not** guarantee consistency with the Watch stream. A write committed via Watch might not be immediately visible to a transaction, and vice versa, due to the distributed nature of the backend. + ## Constraints * **Online Only**: Because they bypass the local cache and require server-side verification, transactions require an active network connection. They will fail if the client is offline. diff --git a/packages/firestore/devdocs/watch.md b/packages/firestore/devdocs/watch.md new file mode 100644 index 0000000000..4acaf7b84f --- /dev/null +++ b/packages/firestore/devdocs/watch.md @@ -0,0 +1,41 @@ +# The Watch System + +This document explains the "Watch" backend system, which powers the real-time capabilities and standard read/write operations of the Firestore SDKs. + +## Overview + +"Watch" is the internal name for the high-scale system that the SDKs interact with. It serves two primary purposes: + +1. **Reads & Writes**: It is the main entry point for standard document operations. +2. **Real-Time Listeners**: It powers the `onSnapshot` live updates by tracking database changes and pushing them to clients. + +## Architecture: The Reverse Proxy + +Watch functions as a massive **Reverse Proxy**. + +1. **Connection**: Massive numbers of end-user devices (phones, browsers) connect directly to Watch. +2. **Routing**: Watch takes the incoming query or write and forwards it to the appropriate underlying storage backend (partition). +3. **Observation**: For queries, Watch doesn't just fetch data once. It "watches" the backend for any writes that would affect the query results and pushes those changes to the subscribed client. + +This architecture allows Firestore to handle millions of concurrent connections while abstracting the complexity of sharding and storage from the client. + +## Consistency Guarantees + +The Watch system (exposed via the `Listen` endpoint) enables strong consistency between reads and writes. + +* **Consistency**: All reads and writes performed through this system are consistent with each other. If you write a document and then immediately read it (or listen to it) via Watch, you will see the latest version. +* **Authentication**: Watch interacts directly with Firebase Auth to identify the user and enforces Firestore Security Rules for every operation. + +> [!IMPORTANT] +> **Transactions** use a different endpoint (`runTransaction`) and **do not** guarantee consistency with the Watch stream. See [Transactions](./transactions.md) for details. + +### Scalability and Client-Side Logic +The Watch system operates at a massive scale. To maintain performance, the backend may rely on the SDK to handle certain query complexities, particularly for **local** execution and consistency. + +> [!NOTE] +> **Composite Queries (OR/IN)** +> While the Watch system supports complex queries (including `OR` and `IN`), the SDK performs significant client-side logic to support them efficiently **locally**. +> * **Local Indexing**: For local cache execution, the SDK transforms composite queries into **Disjunctive Normal Form (DNF)** (breaking them into simpler sub-queries) to utilize simple field indexes. +> * **consistency**: The SDK merges results from the Watch stream and local cache to ensure a consistent view. + +This architectural decision explains why you see complex logic like **Composite Query** execution in the [Query Engine](./query-execution.md). The SDK implements this logic to bridge the gap between user intent and Watch's scalability constraints.