Cloud Firestore data bundles are static data files built by you from
Cloud Firestore document and query snapshots, and published by you on a
CDN, hosting service or other solution. Data bundles include both the
documents you want to provide to your client apps and metadata about the queries
that generated them. You use client SDKs to download bundles over the network or
from local storage, after which you load bundle data to the Cloud Firestore
local cache. Once a bundle is loaded, a client app can query documents from the
local cache or the backend.
With data bundles, your apps can load the results of common queries sooner, since
documents are available at start-up without the need for calls to the
Cloud Firestore backend. If results are loaded from local cache, you also
benefit from reduced access costs. Instead of paying for a million app
instances to query the same initial 100 documents, you pay only for the queries
needed to bundle those 100 documents.
Cloud Firestore data bundles are built to work well with other Firebase
backend products. Take a look at an
integrated solution
in which bundles are built by Cloud Functions and served to users with
Firebase Hosting.
Using a bundle with your app involves three steps:
- Building the bundle with the Admin SDK
- Serving the bundle from local storage or from a CDN
- Loading bundles in the client
What is a data bundle?
A data bundle is a static binary file built by you to package one or more
document and/or query snapshots
and from which you can extract
named
queries
. As we discuss below, the server-side SDKs let you build bundles and
client SDKs provide methods to let you load bundles to the local cache.
Named queries are an especially powerful feature of bundles. Named queries are
Query
objects you can extract from a bundle, then use immediately
to query data either from cache or from the backend, as you do normally in any
part of your app that talks to Cloud Firestore.
Building data bundles on the server
Using the Node.js or Java
Admin SDK
gives you complete
control over what to include in the bundles and how to serve them.
Node.js
var bundleId = "latest-stories";
var bundle = firestore.bundle(bundleId);
var docSnapshot = await firestore.doc('stories/stories').get();
var querySnapshot = await firestore.collection('stories').get();
// Build the bundle
// Note how querySnapshot is named "latest-stories-query"
var bundleBuffer = bundle.add(docSnapshot); // Add a document
.add('latest-stories-query', querySnapshot) // Add a named query.
.build()
Java
Firestore db = FirestoreClient.getFirestore(app);
// Query the 50 latest stories
QuerySnapshot latestStories = db.collection("stories")
.orderBy("timestamp", Direction.DESCENDING)
.limit(50)
.get()
.get();
// Build the bundle from the query results
FirestoreBundle bundle = db.bundleBuilder("latest-stories")
.add("latest-stories-query", latestStories)
.build();
Python
from google.cloud import firestore
from google.cloud.firestore_bundle import FirestoreBundle
db = firestore.Client()
bundle = FirestoreBundle("latest-stories")
doc_snapshot = db.collection("stories").document("news-item").get()
query = db.collection("stories")._query()
# Build the bundle
# Note how `query` is named "latest-stories-query"
bundle_buffer: str = bundle.add_document(doc_snapshot).add_named_query(
"latest-stories-query", query,
).build()
Serving data bundles
You can serve bundles to your client apps from a CDN or by downloading them
from, for example, Cloud Storage.
Assume the bundle created in the previous section has been
saved to a file named
bundle.txt
and posted on a server. This
bundle file is like any other asset you can serve over the web, as shown here
for a simple Node.js Express app.
const fs = require('fs');
const server = require('http').createServer();
server.on('request', (req, res) => {
const src = fs.createReadStream('./bundle.txt');
src.pipe(res);
});
server.listen(8000);
Loading data bundles in the client
You load Firestore bundles by fetching them from a remote server, whether
by making an HTTP request, calling a storage API or using any other technique
for fetching binary files on a network.
Once fetched, using the Cloud Firestore client SDK, your app calls the
loadBundle
method, which returns a task tracking object, the
completion of which you can monitor much as you monitor the status of a Promise.
On successful bundle loading task completion, bundle contents are available in the
local cache.
Web modular API
import { loadBundle, namedQuery, getDocsFromCache } from "firebase/firestore";
async function fetchFromBundle() {
// Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache'
// response header will be set to 'HIT'
const resp = await fetch('/createBundle');
// Load the bundle contents into the Firestore SDK
await loadBundle(db, resp.body);
// Query the results from the cache
const query = await namedQuery(db, 'latest-stories-query');
const storiesSnap = await getDocsFromCache(query);
// Use the results
// ...
}
Web namespaced API
// If you are using module bundlers.
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/firestore/bundle"; // This line enables bundle loading as a side effect.
// ...
async function fetchFromBundle() {
// Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache'
// response header will be set to 'HIT'
const resp = await fetch('/createBundle');
// Load the bundle contents into the Firestore SDK
await db.loadBundle(resp.body);
// Query the results from the cache
// Note: omitting "source: cache" will query the Firestore backend.
const query = await db.namedQuery('latest-stories-query');
const storiesSnap = await query.get({ source: 'cache' });
// Use the results
// ...
}
Swift
Note:
This product is not available on watchOS and App Clip targets.
// Utility function for errors when loading bundles.
func bundleLoadError(reason: String) -> NSError {
return NSError(domain: "FIRSampleErrorDomain",
code: 0,
userInfo: [NSLocalizedFailureReasonErrorKey: reason])
}
func fetchRemoteBundle(for firestore: Firestore,
from url: URL) async throws -> LoadBundleTaskProgress {
guard let inputStream = InputStream(url: url) else {
let error = self.bundleLoadError(reason: "Unable to create stream from the given url: \(url)")
throw error
}
return try await firestore.loadBundle(inputStream)
}
// Fetches a specific named query from the provided bundle.
func loadQuery(named queryName: String,
fromRemoteBundle bundleURL: URL,
with store: Firestore) async throws -> Query {
let _ = try await fetchRemoteBundle(for: store, from: bundleURL)
if let query = await store.getQuery(named: queryName) {
return query
} else {
throw bundleLoadError(reason: "Could not find query named \(queryName)")
}
}
// Load a query and fetch its results from a bundle.
func runStoriesQuery() async {
let queryName = "latest-stories-query"
let firestore = Firestore.firestore()
let remoteBundle = URL(string: "https://example.com/createBundle")!
do {
let query = try await loadQuery(named: queryName,
fromRemoteBundle: remoteBundle,
with: firestore)
let snapshot = try await query.getDocuments()
print(snapshot)
// handle query results
} catch {
print(error)
}
}
Objective-C
Note:
This product is not available on watchOS and App Clip targets.
// Utility function for errors when loading bundles.
- (NSError *)bundleLoadErrorWithReason:(NSString *)reason {
return [NSError errorWithDomain:@"FIRSampleErrorDomain"
code:0
userInfo:@{NSLocalizedFailureReasonErrorKey: reason}];
}
// Loads a remote bundle from the provided url.
- (void)fetchRemoteBundleForFirestore:(FIRFirestore *)firestore
fromURL:(NSURL *)url
completion:(void (^)(FIRLoadBundleTaskProgress *_Nullable,
NSError *_Nullable))completion {
NSInputStream *inputStream = [NSInputStream inputStreamWithURL:url];
if (inputStream == nil) {
// Unable to create input stream.
NSError *error =
[self bundleLoadErrorWithReason:
[NSString stringWithFormat:@"Unable to create stream from the given url: %@", url]];
completion(nil, error);
return;
}
[firestore loadBundleStream:inputStream
completion:^(FIRLoadBundleTaskProgress * _Nullable progress,
NSError * _Nullable error) {
if (progress == nil) {
completion(nil, error);
return;
}
if (progress.state == FIRLoadBundleTaskStateSuccess) {
completion(progress, nil);
} else {
NSError *concreteError =
[self bundleLoadErrorWithReason:
[NSString stringWithFormat:
@"Expected bundle load to be completed, but got %ld instead",
(long)progress.state]];
completion(nil, concreteError);
}
completion(nil, nil);
}];
}
// Loads a bundled query.
- (void)loadQueryNamed:(NSString *)queryName
fromRemoteBundleURL:(NSURL *)url
withFirestore:(FIRFirestore *)firestore
completion:(void (^)(FIRQuery *_Nullable, NSError *_Nullable))completion {
[self fetchRemoteBundleForFirestore:firestore
fromURL:url
completion:^(FIRLoadBundleTaskProgress *progress, NSError *error) {
if (error != nil) {
completion(nil, error);
return;
}
[firestore getQueryNamed:queryName completion:^(FIRQuery *query) {
if (query == nil) {
NSString *errorReason =
[NSString stringWithFormat:@"Could not find query named %@", queryName];
NSError *error = [self bundleLoadErrorWithReason:errorReason];
completion(nil, error);
return;
}
completion(query, nil);
}];
}];
}
- (void)runStoriesQuery {
NSString *queryName = @"latest-stories-query";
FIRFirestore *firestore = [FIRFirestore firestore];
NSURL *bundleURL = [NSURL URLWithString:@"https://example.com/createBundle"];
[self loadQueryNamed:queryName
fromRemoteBundleURL:bundleURL
withFirestore:firestore
completion:^(FIRQuery *query, NSError *error) {
// Handle query results
}];
}
Kotlin+KTX
@Throws(IOException::class)
fun getBundleStream(urlString: String?): InputStream {
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
return connection.inputStream
}
@Throws(IOException::class)
fun fetchFromBundle() {
val bundleStream = getBundleStream("https://example.com/createBundle")
val loadTask = db.loadBundle(bundleStream)
// Chain the following tasks
// 1) Load the bundle
// 2) Get the named query from the local cache
// 3) Execute a get() on the named query
loadTask.continueWithTask<Query> { task ->
// Close the stream
bundleStream.close()
// Calling .result propagates errors
val progress = task.getResult(Exception::class.java)
// Get the named query from the bundle cache
db.getNamedQuery("latest-stories-query")
}.continueWithTask { task ->
val query = task.getResult(Exception::class.java)!!
// get() the query results from the cache
query.get(Source.CACHE)
}.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w(TAG, "Bundle loading failed", task.exception)
return@addOnCompleteListener
}
// Get the QuerySnapshot from the bundle
val storiesSnap = task.result
// Use the results
// ...
}
}
Java
public InputStream getBundleStream(String urlString) throws IOException {
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
return connection.getInputStream();
}
public void fetchBundleFrom() throws IOException {
final InputStream bundleStream = getBundleStream("https://example.com/createBundle");
LoadBundleTask loadTask = db.loadBundle(bundleStream);
// Chain the following tasks
// 1) Load the bundle
// 2) Get the named query from the local cache
// 3) Execute a get() on the named query
loadTask.continueWithTask(new Continuation<LoadBundleTaskProgress, Task<Query>>() {
@Override
public Task<Query> then(@NonNull Task<LoadBundleTaskProgress> task) throws Exception {
// Close the stream
bundleStream.close();
// Calling getResult() propagates errors
LoadBundleTaskProgress progress = task.getResult(Exception.class);
// Get the named query from the bundle cache
return db.getNamedQuery("latest-stories-query");
}
}).continueWithTask(new Continuation<Query, Task<QuerySnapshot>>() {
@Override
public Task<QuerySnapshot> then(@NonNull Task<Query> task) throws Exception {
Query query = task.getResult(Exception.class);
// get() the query results from the cache
return query.get(Source.CACHE);
}
}).addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
if (!task.isSuccessful()) {
Log.w(TAG, "Bundle loading failed", task.getException());
return;
}
// Get the QuerySnapshot from the bundle
QuerySnapshot storiesSnap = task.getResult();
// Use the results
// ...
}
});
}
C++
db->LoadBundle("bundle_name", [](const LoadBundleTaskProgress& progress) {
switch(progress.state()) {
case LoadBundleTaskProgress::State::kError: {
// The bundle load has errored. Handle the error in the returned future.
return;
}
case LoadBundleTaskProgress::State::kInProgress: {
std::cout << "Bytes loaded from bundle: " << progress.bytes_loaded()
<< std::endl;
break;
}
case LoadBundleTaskProgress::State::kSuccess: {
std::cout << "Bundle load succeeeded" << std::endl;
break;
}
}
}).OnCompletion([db](const Future<LoadBundleTaskProgress>& future) {
if (future.error() != Error::kErrorOk) {
// Handle error...
return;
}
const std::string& query_name = "latest_stories_query";
db->NamedQuery(query_name).OnCompletion([](const Future<Query>& query_future){
if (query_future.error() != Error::kErrorOk) {
// Handle error...
return;
}
const Query* query = query_future.result();
query->Get().OnCompletion([](const Future<QuerySnapshot> &){
// ...
});
});
});
Note that if you load a named query from a bundle built less than 30 minutes
prior, once you use it to read from the backend rather than cache, you
will only pay for database reads needed to update documents to match what is
stored on the backend; that is, you only pay for the deltas.
What next?