Skip to content

Commit 34973cd

Browse files
authored
Query get() method for RTDB (#3812)
1 parent d5baaff commit 34973cd

File tree

8 files changed

+289
-5
lines changed

8 files changed

+289
-5
lines changed

.changeset/many-snails-kneel.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@firebase/database": minor
3+
"firebase": minor
4+
---
5+
6+
Add a `get` method for database queries that returns server result when connected

packages/database/src/api/Query.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,13 @@ export class Query {
295295
this.repo.removeEventCallbackForQuery(this, container);
296296
}
297297

298+
/**
299+
* Get the server-value for this query, or return a cached value if not connected.
300+
*/
301+
get(): Promise<DataSnapshot> {
302+
return this.repo.getValue(this);
303+
}
304+
298305
/**
299306
* Attaches a listener, waits for the first event, and then removes the listener
300307
* @param {!string} eventType

packages/database/src/core/PersistentConnection.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import {
2626
isValidFormat,
2727
isMobileCordova,
2828
isReactNative,
29-
isNodeSdk
29+
isNodeSdk,
30+
Deferred
3031
} from '@firebase/util';
3132

3233
import { error, log, logWrapper, warn, ObjectToUniqueKey } from './util/util';
@@ -44,6 +45,7 @@ import { SDK_VERSION } from './version';
4445

4546
const RECONNECT_MIN_DELAY = 1000;
4647
const RECONNECT_MAX_DELAY_DEFAULT = 60 * 5 * 1000; // 5 minutes in milliseconds (Case: 1858)
48+
const GET_CONNECT_TIMEOUT = 3 * 1000;
4749
const RECONNECT_MAX_DELAY_FOR_ADMINS = 30 * 1000; // 30 seconds for admin clients (likely to be a backend server)
4850
const RECONNECT_DELAY_MULTIPLIER = 1.3;
4951
const RECONNECT_DELAY_RESET_TIMEOUT = 30000; // Reset delay back to MIN_DELAY after being connected for 30sec.
@@ -75,6 +77,11 @@ interface OutstandingPut {
7577
onComplete: (a: string, b?: string) => void;
7678
}
7779

80+
interface OutstandingGet {
81+
request: object;
82+
onComplete: (response: { [k: string]: unknown }) => void;
83+
}
84+
7885
/**
7986
* Firebase connection. Abstracts wire protocol and handles reconnecting.
8087
*
@@ -93,7 +100,9 @@ export class PersistentConnection extends ServerActions {
93100
Map</* queryId */ string, ListenSpec>
94101
> = new Map();
95102
private outstandingPuts_: OutstandingPut[] = [];
103+
private outstandingGets_: OutstandingGet[] = [];
96104
private outstandingPutCount_ = 0;
105+
private outstandingGetCount_ = 0;
97106
private onDisconnectRequestQueue_: OnDisconnectRequest[] = [];
98107
private connected_ = false;
99108
private reconnectDelay_ = RECONNECT_MIN_DELAY;
@@ -184,6 +193,57 @@ export class PersistentConnection extends ServerActions {
184193
}
185194
}
186195

196+
get(query: Query): Promise<string> {
197+
const deferred = new Deferred<string>();
198+
const request = {
199+
p: query.path.toString(),
200+
q: query.queryObject()
201+
};
202+
const outstandingGet = {
203+
action: 'g',
204+
request,
205+
onComplete: (message: { [k: string]: unknown }) => {
206+
const payload = message['d'] as string;
207+
if (message['s'] === 'ok') {
208+
this.onDataUpdate_(
209+
request['p'],
210+
payload,
211+
/*isMerge*/ false,
212+
/*tag*/ null
213+
);
214+
deferred.resolve(payload);
215+
} else {
216+
deferred.reject(payload);
217+
}
218+
}
219+
};
220+
this.outstandingGets_.push(outstandingGet);
221+
this.outstandingGetCount_++;
222+
const index = this.outstandingGets_.length - 1;
223+
224+
if (!this.connected_) {
225+
setTimeout(() => {
226+
const get = this.outstandingGets_[index];
227+
if (get === undefined || outstandingGet !== get) {
228+
return;
229+
}
230+
delete this.outstandingGets_[index];
231+
this.outstandingGetCount_--;
232+
if (this.outstandingGetCount_ === 0) {
233+
this.outstandingGets_ = [];
234+
}
235+
this.log_('get ' + index + ' timed out on connection');
236+
deferred.reject(new Error('Client is offline.'));
237+
}, GET_CONNECT_TIMEOUT);
238+
}
239+
240+
if (this.connected_) {
241+
this.sendGet_(index);
242+
}
243+
244+
return deferred.promise;
245+
}
246+
187247
/**
188248
* @inheritDoc
189249
*/
@@ -221,6 +281,20 @@ export class PersistentConnection extends ServerActions {
221281
}
222282
}
223283

284+
private sendGet_(index: number) {
285+
const get = this.outstandingGets_[index];
286+
this.sendRequest('g', get.request, (message: { [k: string]: unknown }) => {
287+
delete this.outstandingGets_[index];
288+
this.outstandingGetCount_--;
289+
if (this.outstandingGetCount_ === 0) {
290+
this.outstandingGets_ = [];
291+
}
292+
if (get.onComplete) {
293+
get.onComplete(message);
294+
}
295+
});
296+
}
297+
224298
private sendListen_(listenSpec: ListenSpec) {
225299
const query = listenSpec.query;
226300
const pathString = query.path.toString();
@@ -950,6 +1024,12 @@ export class PersistentConnection extends ServerActions {
9501024
request.onComplete
9511025
);
9521026
}
1027+
1028+
for (let i = 0; i < this.outstandingGets_.length; i++) {
1029+
if (this.outstandingGets_[i]) {
1030+
this.sendGet_(i);
1031+
}
1032+
}
9531033
}
9541034

9551035
/**

packages/database/src/core/ReadonlyRestClient.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { assert, jsonEval, safeGet, querystring } from '@firebase/util';
18+
import {
19+
assert,
20+
jsonEval,
21+
safeGet,
22+
querystring,
23+
Deferred
24+
} from '@firebase/util';
1925
import { logWrapper, warn } from './util/util';
2026

2127
import { ServerActions } from './ServerActions';
@@ -139,6 +145,42 @@ export class ReadonlyRestClient extends ServerActions {
139145
delete this.listens_[listenId];
140146
}
141147

148+
get(query: Query): Promise<string> {
149+
const queryStringParameters = query
150+
.getQueryParams()
151+
.toRestQueryStringParameters();
152+
153+
const pathString = query.path.toString();
154+
155+
const deferred = new Deferred<string>();
156+
157+
this.restRequest_(
158+
pathString + '.json',
159+
queryStringParameters,
160+
(error, result) => {
161+
let data = result;
162+
163+
if (error === 404) {
164+
data = null;
165+
error = null;
166+
}
167+
168+
if (error === null) {
169+
this.onDataUpdate_(
170+
pathString,
171+
data,
172+
/*isMerge=*/ false,
173+
/*tag=*/ null
174+
);
175+
deferred.resolve(data as string);
176+
} else {
177+
deferred.reject(new Error(data as string));
178+
}
179+
}
180+
);
181+
return deferred.promise;
182+
}
183+
142184
/** @inheritDoc */
143185
refreshAuthToken(token: string) {
144186
// no-op since we just always call getToken.

packages/database/src/core/Repo.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { ReadonlyRestClient } from './ReadonlyRestClient';
3838
import { FirebaseApp } from '@firebase/app-types';
3939
import { RepoInfo } from './RepoInfo';
4040
import { Database } from '../api/Database';
41+
import { DataSnapshot } from '../api/DataSnapshot';
4142
import { ServerActions } from './ServerActions';
4243
import { Query } from '../api/Query';
4344
import { EventRegistration } from './view/EventRegistration';
@@ -304,6 +305,71 @@ export class Repo {
304305
return this.nextWriteId_++;
305306
}
306307

308+
/**
309+
* The purpose of `getValue` is to return the latest known value
310+
* satisfying `query`.
311+
*
312+
* If the client is connected, this method will send a request
313+
* to the server. If the client is not connected, then either:
314+
*
315+
* 1. The client was once connected, but not anymore.
316+
* 2. The client has never connected, this is the first operation
317+
* this repo is handling.
318+
*
319+
* In case (1), it's possible that the client still has an active
320+
* listener, with cached data. Since this is the latest known
321+
* value satisfying the query, that's what getValue will return.
322+
* If there is no cached data, `getValue` surfaces an "offline"
323+
* error.
324+
*
325+
* In case (2), `getValue` will trigger a time-limited connection
326+
* attempt. If the client is unable to connect to the server, it
327+
* will surface an "offline" error because there cannot be any
328+
* cached data. On the other hand, if the client is able to connect,
329+
* `getValue` will return the server's value for the query, if one
330+
* exists.
331+
*
332+
* @param query - The query to surface a value for.
333+
*/
334+
getValue(query: Query): Promise<DataSnapshot> {
335+
return this.server_.get(query).then(
336+
payload => {
337+
const node = nodeFromJSON(payload as string);
338+
const events = this.serverSyncTree_.applyServerOverwrite(
339+
query.path,
340+
node
341+
);
342+
this.eventQueue_.raiseEventsAtPath(query.path, events);
343+
return Promise.resolve(
344+
new DataSnapshot(
345+
node,
346+
query.getRef(),
347+
query.getQueryParams().getIndex()
348+
)
349+
);
350+
},
351+
err => {
352+
this.log_(
353+
'get for query ' +
354+
stringify(query) +
355+
' falling back to cache after error: ' +
356+
err
357+
);
358+
const cached = this.serverSyncTree_.calcCompleteEventCache(query.path);
359+
if (!cached.isEmpty()) {
360+
return Promise.resolve(
361+
new DataSnapshot(
362+
cached,
363+
query.getRef(),
364+
query.getQueryParams().getIndex()
365+
)
366+
);
367+
}
368+
return Promise.reject(new Error(err as string));
369+
}
370+
);
371+
}
372+
307373
setWithPriority(
308374
path: Path,
309375
newVal: unknown,

packages/database/src/core/ServerActions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ export abstract class ServerActions {
4545
*/
4646
abstract unlisten(query: Query, tag: number | null): void;
4747

48+
/**
49+
* Get the server value satisfying this query.
50+
*/
51+
abstract get(query: Query): Promise<string>;
52+
4853
/**
4954
* @param {string} pathString
5055
* @param {*} data

0 commit comments

Comments
 (0)