-
Notifications
You must be signed in to change notification settings - Fork 904
/
Copy pathdatabase.ts
638 lines (599 loc) · 21.9 KB
/
database.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
_getProvider,
_removeServiceInstance,
FirebaseApp,
getApp
} from '@firebase/app';
import { deepEqual, getDefaultEmulatorHostnameAndPort } from '@firebase/util';
import { User } from '../auth/user';
import {
IndexedDbOfflineComponentProvider,
MultiTabOfflineComponentProvider,
OfflineComponentProvider,
OfflineComponentProviderFactory,
OnlineComponentProvider,
OnlineComponentProviderFactory
} from '../core/component_provider';
import { DatabaseId, DEFAULT_DATABASE_NAME } from '../core/database_info';
import {
FirestoreClient,
firestoreClientDisableNetwork,
firestoreClientEnableNetwork,
firestoreClientGetNamedQuery,
firestoreClientLoadBundle,
firestoreClientWaitForPendingWrites
} from '../core/firestore_client';
import { makeDatabaseInfo } from '../lite-api/components';
import {
connectFirestoreEmulator,
Firestore as LiteFirestore
} from '../lite-api/database';
import { Query } from '../lite-api/reference';
import {
indexedDbClearPersistence,
indexedDbStoragePrefix
} from '../local/indexeddb_persistence';
import { LRU_COLLECTION_DISABLED } from '../local/lru_garbage_collector';
import { LRU_MINIMUM_CACHE_SIZE_BYTES } from '../local/lru_garbage_collector_impl';
import { debugAssert } from '../util/assert';
import { AsyncQueue } from '../util/async_queue';
import { AsyncQueueImpl } from '../util/async_queue_impl';
import { Code, FirestoreError } from '../util/error';
import { cast } from '../util/input_validation';
import { logWarn } from '../util/log';
import { Deferred } from '../util/promise';
import { LoadBundleTask } from './bundle';
import { CredentialsProvider } from './credentials';
import { FirestoreSettings, PersistenceSettings } from './settings';
export {
connectFirestoreEmulator,
EmulatorMockTokenOptions
} from '../lite-api/database';
declare module '@firebase/component' {
interface NameServiceMapping {
'firestore': Firestore;
}
}
/**
* Constant used to indicate the LRU garbage collection should be disabled.
* Set this value as the `cacheSizeBytes` on the settings passed to the
* {@link Firestore} instance.
*/
export const CACHE_SIZE_UNLIMITED = LRU_COLLECTION_DISABLED;
/**
* The Cloud Firestore service interface.
*
* Do not call this constructor directly. Instead, use {@link (getFirestore:1)}.
*/
export class Firestore extends LiteFirestore {
/**
* Whether it's a {@link Firestore} or Firestore Lite instance.
*/
type: 'firestore-lite' | 'firestore' = 'firestore';
_queue: AsyncQueue = new AsyncQueueImpl();
readonly _persistenceKey: string;
_firestoreClient: FirestoreClient | undefined;
_componentsProvider?: {
_offline: OfflineComponentProviderFactory;
_online: OnlineComponentProviderFactory;
};
/** @hideconstructor */
constructor(
authCredentialsProvider: CredentialsProvider<User>,
appCheckCredentialsProvider: CredentialsProvider<string>,
databaseId: DatabaseId,
app?: FirebaseApp
) {
super(
authCredentialsProvider,
appCheckCredentialsProvider,
databaseId,
app
);
this._persistenceKey = app?.name || '[DEFAULT]';
}
protected async _terminate(): Promise<void> {
if (this._firestoreClient) {
const terminate = this._firestoreClient.terminate();
this._queue = new AsyncQueueImpl(terminate);
this._firestoreClient = undefined;
await terminate;
}
}
}
/**
* Initializes a new instance of {@link Firestore} with the provided settings.
* Can only be called before any other function, including
* {@link (getFirestore:1)}. If the custom settings are empty, this function is
* equivalent to calling {@link (getFirestore:1)}.
*
* @param app - The {@link @firebase/app#FirebaseApp} with which the {@link Firestore} instance will
* be associated.
* @param settings - A settings object to configure the {@link Firestore} instance.
* @param databaseId - The name of the database.
* @returns A newly initialized {@link Firestore} instance.
*/
export function initializeFirestore(
app: FirebaseApp,
settings: FirestoreSettings,
databaseId?: string
): Firestore {
if (!databaseId) {
databaseId = DEFAULT_DATABASE_NAME;
}
const provider = _getProvider(app, 'firestore');
if (provider.isInitialized(databaseId)) {
const existingInstance = provider.getImmediate({
identifier: databaseId
});
const initialSettings = provider.getOptions(
databaseId
) as FirestoreSettings;
if (deepEqual(initialSettings, settings)) {
return existingInstance;
} else {
throw new FirestoreError(
Code.FAILED_PRECONDITION,
'initializeFirestore() has already been called with ' +
'different options. To avoid this error, call initializeFirestore() with the ' +
'same options as when it was originally called, or call getFirestore() to return the' +
' already initialized instance.'
);
}
}
if (
settings.cacheSizeBytes !== undefined &&
settings.localCache !== undefined
) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
`cache and cacheSizeBytes cannot be specified at the same time as cacheSizeBytes will` +
`be deprecated. Instead, specify the cache size in the cache object`
);
}
if (
settings.cacheSizeBytes !== undefined &&
settings.cacheSizeBytes !== CACHE_SIZE_UNLIMITED &&
settings.cacheSizeBytes < LRU_MINIMUM_CACHE_SIZE_BYTES
) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
`cacheSizeBytes must be at least ${LRU_MINIMUM_CACHE_SIZE_BYTES}`
);
}
return provider.initialize({
options: settings,
instanceIdentifier: databaseId
});
}
/**
* Returns the existing default {@link Firestore} instance that is associated with the
* default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
* instance with default settings.
*
* @returns The default {@link Firestore} instance of the default app.
*/
export function getFirestore(): Firestore;
/**
* Returns the existing default {@link Firestore} instance that is associated with the
* provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
* instance with default settings.
*
* @param app - The {@link @firebase/app#FirebaseApp} instance that the returned {@link Firestore}
* instance is associated with.
* @returns The default {@link Firestore} instance of the provided app.
*/
export function getFirestore(app: FirebaseApp): Firestore;
/**
* Returns the existing named {@link Firestore} instance that is associated with the
* default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
* instance with default settings.
*
* @param databaseId - The name of the database.
* @returns The named {@link Firestore} instance of the default app.
* @beta
*/
export function getFirestore(databaseId: string): Firestore;
/**
* Returns the existing named {@link Firestore} instance that is associated with the
* provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
* instance with default settings.
*
* @param app - The {@link @firebase/app#FirebaseApp} instance that the returned {@link Firestore}
* instance is associated with.
* @param databaseId - The name of the database.
* @returns The named {@link Firestore} instance of the provided app.
* @beta
*/
export function getFirestore(app: FirebaseApp, databaseId: string): Firestore;
export function getFirestore(
appOrDatabaseId?: FirebaseApp | string,
optionalDatabaseId?: string
): Firestore {
const app: FirebaseApp =
typeof appOrDatabaseId === 'object' ? appOrDatabaseId : getApp();
const databaseId =
typeof appOrDatabaseId === 'string'
? appOrDatabaseId
: optionalDatabaseId || DEFAULT_DATABASE_NAME;
const db = _getProvider(app, 'firestore').getImmediate({
identifier: databaseId
}) as Firestore;
if (!db._initialized) {
const emulator = getDefaultEmulatorHostnameAndPort('firestore');
if (emulator) {
connectFirestoreEmulator(db, ...emulator);
}
}
return db;
}
/**
* @internal
*/
export function ensureFirestoreConfigured(
firestore: Firestore
): FirestoreClient {
if (firestore._terminated) {
throw new FirestoreError(
Code.FAILED_PRECONDITION,
'The client has already been terminated.'
);
}
if (!firestore._firestoreClient) {
configureFirestore(firestore);
}
return firestore._firestoreClient as FirestoreClient;
}
export function configureFirestore(firestore: Firestore): void {
const settings = firestore._freezeSettings();
debugAssert(!!settings.host, 'FirestoreSettings.host is not set');
debugAssert(
!firestore._firestoreClient,
'configureFirestore() called multiple times'
);
const databaseInfo = makeDatabaseInfo(
firestore._databaseId,
firestore._app?.options.appId || '',
firestore._persistenceKey,
settings
);
if (!firestore._componentsProvider) {
if (
settings.localCache?._offlineComponentProvider &&
settings.localCache?._onlineComponentProvider
) {
firestore._componentsProvider = {
_offline: settings.localCache._offlineComponentProvider,
_online: settings.localCache._onlineComponentProvider
};
}
}
firestore._firestoreClient = new FirestoreClient(
firestore._authCredentials,
firestore._appCheckCredentials,
firestore._queue,
databaseInfo,
firestore._componentsProvider &&
buildComponentProvider(firestore._componentsProvider)
);
}
function buildComponentProvider(componentsProvider: {
_offline: OfflineComponentProviderFactory;
_online: OnlineComponentProviderFactory;
}): {
_offline: OfflineComponentProvider;
_online: OnlineComponentProvider;
} {
const online = componentsProvider?._online.build();
return {
_offline: componentsProvider?._offline.build(online),
_online: online
};
}
/**
* Attempts to enable persistent storage, if possible.
*
* On failure, `enableIndexedDbPersistence()` will reject the promise or
* throw an exception. There are several reasons why this can fail, which can be
* identified by the `code` on the error.
*
* * failed-precondition: The app is already open in another browser tab.
* * unimplemented: The browser is incompatible with the offline persistence
* implementation.
*
* Note that even after a failure, the {@link Firestore} instance will remain
* usable, however offline persistence will be disabled.
*
* Note: `enableIndexedDbPersistence()` must be called before any other functions
* (other than {@link initializeFirestore}, {@link (getFirestore:1)} or
* {@link clearIndexedDbPersistence}.
*
* Persistence cannot be used in a Node.js environment.
*
* @param firestore - The {@link Firestore} instance to enable persistence for.
* @param persistenceSettings - Optional settings object to configure
* persistence.
* @returns A `Promise` that represents successfully enabling persistent storage.
* @deprecated This function will be removed in a future major release. Instead, set
* `FirestoreSettings.localCache` to an instance of `PersistentLocalCache` to
* turn on IndexedDb cache. Calling this function when `FirestoreSettings.localCache`
* is already specified will throw an exception.
*/
export function enableIndexedDbPersistence(
firestore: Firestore,
persistenceSettings?: PersistenceSettings
): Promise<void> {
logWarn(
'enableIndexedDbPersistence() will be deprecated in the future, ' +
'you can use `FirestoreSettings.cache` instead.'
);
const settings = firestore._freezeSettings();
setPersistenceProviders(firestore, OnlineComponentProvider.provider, {
build: (onlineComponents: OnlineComponentProvider) =>
new IndexedDbOfflineComponentProvider(
onlineComponents,
settings.cacheSizeBytes,
persistenceSettings?.forceOwnership
)
});
return Promise.resolve();
}
/**
* Attempts to enable multi-tab persistent storage, if possible. If enabled
* across all tabs, all operations share access to local persistence, including
* shared execution of queries and latency-compensated local document updates
* across all connected instances.
*
* On failure, `enableMultiTabIndexedDbPersistence()` will reject the promise or
* throw an exception. There are several reasons why this can fail, which can be
* identified by the `code` on the error.
*
* * failed-precondition: The app is already open in another browser tab and
* multi-tab is not enabled.
* * unimplemented: The browser is incompatible with the offline persistence
* implementation.
*
* Note that even after a failure, the {@link Firestore} instance will remain
* usable, however offline persistence will be disabled.
*
* @param firestore - The {@link Firestore} instance to enable persistence for.
* @returns A `Promise` that represents successfully enabling persistent
* storage.
* @deprecated This function will be removed in a future major release. Instead, set
* `FirestoreSettings.localCache` to an instance of `PersistentLocalCache` to
* turn on indexeddb cache. Calling this function when `FirestoreSettings.localCache`
* is already specified will throw an exception.
*/
export async function enableMultiTabIndexedDbPersistence(
firestore: Firestore
): Promise<void> {
logWarn(
'enableMultiTabIndexedDbPersistence() will be deprecated in the future, ' +
'you can use `FirestoreSettings.cache` instead.'
);
const settings = firestore._freezeSettings();
setPersistenceProviders(firestore, OnlineComponentProvider.provider, {
build: (onlineComponents: OnlineComponentProvider) =>
new MultiTabOfflineComponentProvider(
onlineComponents,
settings.cacheSizeBytes
)
});
}
/**
* Registers both the `OfflineComponentProvider` and `OnlineComponentProvider`.
* If the operation fails with a recoverable error (see
* `canRecoverFromIndexedDbError()` below), the returned Promise is rejected
* but the client remains usable.
*/
function setPersistenceProviders(
firestore: Firestore,
onlineComponentProvider: OnlineComponentProviderFactory,
offlineComponentProvider: OfflineComponentProviderFactory
): void {
firestore = cast(firestore, Firestore);
if (firestore._firestoreClient || firestore._terminated) {
throw new FirestoreError(
Code.FAILED_PRECONDITION,
'Firestore has already been started and persistence can no longer be ' +
'enabled. You can only enable persistence before calling any other ' +
'methods on a Firestore object.'
);
}
if (firestore._componentsProvider || firestore._getSettings().localCache) {
throw new FirestoreError(
Code.FAILED_PRECONDITION,
'SDK cache is already specified.'
);
}
firestore._componentsProvider = {
_online: onlineComponentProvider,
_offline: offlineComponentProvider
};
configureFirestore(firestore);
}
/**
* Clears the persistent storage. This includes pending writes and cached
* documents.
*
* Must be called while the {@link Firestore} instance is not started (after the app is
* terminated or when the app is first initialized). On startup, this function
* must be called before other functions (other than {@link
* initializeFirestore} or {@link (getFirestore:1)})). If the {@link Firestore}
* instance is still running, the promise will be rejected with the error code
* of `failed-precondition`.
*
* Note: `clearIndexedDbPersistence()` is primarily intended to help write
* reliable tests that use Cloud Firestore. It uses an efficient mechanism for
* dropping existing data but does not attempt to securely overwrite or
* otherwise make cached data unrecoverable. For applications that are sensitive
* to the disclosure of cached data in between user sessions, we strongly
* recommend not enabling persistence at all.
*
* @param firestore - The {@link Firestore} instance to clear persistence for.
* @returns A `Promise` that is resolved when the persistent storage is
* cleared. Otherwise, the promise is rejected with an error.
*/
export function clearIndexedDbPersistence(firestore: Firestore): Promise<void> {
if (firestore._initialized && !firestore._terminated) {
throw new FirestoreError(
Code.FAILED_PRECONDITION,
'Persistence can only be cleared before a Firestore instance is ' +
'initialized or after it is terminated.'
);
}
const deferred = new Deferred<void>();
firestore._queue.enqueueAndForgetEvenWhileRestricted(async () => {
try {
await indexedDbClearPersistence(
indexedDbStoragePrefix(firestore._databaseId, firestore._persistenceKey)
);
deferred.resolve();
} catch (e) {
deferred.reject(e as Error | undefined);
}
});
return deferred.promise;
}
/**
* Waits until all currently pending writes for the active user have been
* acknowledged by the backend.
*
* The returned promise resolves immediately if there are no outstanding writes.
* Otherwise, the promise waits for all previously issued writes (including
* those written in a previous app session), but it does not wait for writes
* that were added after the function is called. If you want to wait for
* additional writes, call `waitForPendingWrites()` again.
*
* Any outstanding `waitForPendingWrites()` promises are rejected during user
* changes.
*
* @returns A `Promise` which resolves when all currently pending writes have been
* acknowledged by the backend.
*/
export function waitForPendingWrites(firestore: Firestore): Promise<void> {
firestore = cast(firestore, Firestore);
const client = ensureFirestoreConfigured(firestore);
return firestoreClientWaitForPendingWrites(client);
}
/**
* Re-enables use of the network for this {@link Firestore} instance after a prior
* call to {@link disableNetwork}.
*
* @returns A `Promise` that is resolved once the network has been enabled.
*/
export function enableNetwork(firestore: Firestore): Promise<void> {
firestore = cast(firestore, Firestore);
const client = ensureFirestoreConfigured(firestore);
return firestoreClientEnableNetwork(client);
}
/**
* Disables network usage for this instance. It can be re-enabled via {@link
* enableNetwork}. While the network is disabled, any snapshot listeners,
* `getDoc()` or `getDocs()` calls will return results from cache, and any write
* operations will be queued until the network is restored.
*
* @returns A `Promise` that is resolved once the network has been disabled.
*/
export function disableNetwork(firestore: Firestore): Promise<void> {
firestore = cast(firestore, Firestore);
const client = ensureFirestoreConfigured(firestore);
return firestoreClientDisableNetwork(client);
}
/**
* Terminates the provided {@link Firestore} instance.
*
* After calling `terminate()` only the `clearIndexedDbPersistence()` function
* may be used. Any other function will throw a `FirestoreError`.
*
* To restart after termination, create a new instance of FirebaseFirestore with
* {@link (getFirestore:1)}.
*
* Termination does not cancel any pending writes, and any promises that are
* awaiting a response from the server will not be resolved. If you have
* persistence enabled, the next time you start this instance, it will resume
* sending these writes to the server.
*
* Note: Under normal circumstances, calling `terminate()` is not required. This
* function is useful only when you want to force this instance to release all
* of its resources or in combination with `clearIndexedDbPersistence()` to
* ensure that all local state is destroyed between test runs.
*
* @returns A `Promise` that is resolved when the instance has been successfully
* terminated.
*/
export function terminate(firestore: Firestore): Promise<void> {
_removeServiceInstance(
firestore.app,
'firestore',
firestore._databaseId.database
);
return firestore._delete();
}
/**
* Loads a Firestore bundle into the local cache.
*
* @param firestore - The {@link Firestore} instance to load bundles for.
* @param bundleData - An object representing the bundle to be loaded. Valid
* objects are `ArrayBuffer`, `ReadableStream<Uint8Array>` or `string`.
*
* @returns A `LoadBundleTask` object, which notifies callers with progress
* updates, and completion or error events. It can be used as a
* `Promise<LoadBundleTaskProgress>`.
*/
export function loadBundle(
firestore: Firestore,
bundleData: ReadableStream<Uint8Array> | ArrayBuffer | string
): LoadBundleTask {
firestore = cast(firestore, Firestore);
const client = ensureFirestoreConfigured(firestore);
const resultTask = new LoadBundleTask();
firestoreClientLoadBundle(
client,
firestore._databaseId,
bundleData,
resultTask
);
return resultTask;
}
/**
* Reads a Firestore {@link Query} from local cache, identified by the given
* name.
*
* The named queries are packaged into bundles on the server side (along
* with resulting documents), and loaded to local cache using `loadBundle`. Once
* in local cache, use this method to extract a {@link Query} by name.
*
* @param firestore - The {@link Firestore} instance to read the query from.
* @param name - The name of the query.
* @returns A `Promise` that is resolved with the Query or `null`.
*/
export function namedQuery(
firestore: Firestore,
name: string
): Promise<Query | null> {
firestore = cast(firestore, Firestore);
const client = ensureFirestoreConfigured(firestore);
return firestoreClientGetNamedQuery(client, name).then(namedQuery => {
if (!namedQuery) {
return null;
}
return new Query(firestore, null, namedQuery.query);
});
}