Skip to content

Commit cb81ae6

Browse files
authored
Merge c0f627c into 8697166
2 parents 8697166 + c0f627c commit cb81ae6

File tree

8 files changed

+275
-12
lines changed

8 files changed

+275
-12
lines changed

.changeset/many-snails-kneel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/database": minor
3+
---
4+
5+
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: 94 additions & 8 deletions
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,12 @@ interface OutstandingPut {
7577
onComplete: (a: string, b?: string) => void;
7678
}
7779

80+
interface OutstandingGet {
81+
action: string;
82+
request: object;
83+
onComplete: (response: { [k: string]: unknown }) => void;
84+
}
85+
7886
/**
7987
* Firebase connection. Abstracts wire protocol and handles reconnecting.
8088
*
@@ -93,7 +101,9 @@ export class PersistentConnection extends ServerActions {
93101
Map</* queryId */ string, ListenSpec>
94102
> = new Map();
95103
private outstandingPuts_: OutstandingPut[] = [];
104+
private outstandingGets_: OutstandingGet[] = [];
96105
private outstandingPutCount_ = 0;
106+
private outstandingGetCount_ = 0;
97107
private onDisconnectRequestQueue_: OnDisconnectRequest[] = [];
98108
private connected_ = false;
99109
private reconnectDelay_ = RECONNECT_MIN_DELAY;
@@ -184,6 +194,58 @@ export class PersistentConnection extends ServerActions {
184194
}
185195
}
186196

197+
get(query: Query): Promise<string> {
198+
const deferred = new Deferred<string>();
199+
const request = {
200+
p: query.path.toString(),
201+
q: query.queryObject()
202+
};
203+
const outstandingGet = {
204+
action: 'g',
205+
request,
206+
onComplete: (message: { [k: string]: unknown }) => {
207+
const payload = message['d'] as string;
208+
if (message['s'] === 'ok') {
209+
this.onDataUpdate_(
210+
request['p'],
211+
payload,
212+
/*isMerge*/ false,
213+
/*tag*/ null
214+
);
215+
deferred.resolve(payload);
216+
} else {
217+
deferred.reject(payload);
218+
}
219+
}
220+
};
221+
this.outstandingGets_.push(outstandingGet);
222+
this.outstandingGetCount_++;
223+
const index = this.outstandingGets_.length - 1;
224+
225+
if (!this.connected_) {
226+
const self = this;
227+
setTimeout(() => {
228+
const get = self.outstandingGets_[index];
229+
if (get === undefined || outstandingGet !== get) {
230+
return;
231+
}
232+
delete self.outstandingGets_[index];
233+
self.outstandingGetCount_--;
234+
if (self.outstandingGetCount_ === 0) {
235+
self.outstandingGets_ = [];
236+
}
237+
self.log_('get ' + index + ' timed out on connection');
238+
deferred.reject(new Error('Client is offline.'));
239+
}, GET_CONNECT_TIMEOUT);
240+
}
241+
242+
if (this.connected_) {
243+
this.sendGet_(index);
244+
}
245+
246+
return deferred.promise;
247+
}
248+
187249
/**
188250
* @inheritDoc
189251
*/
@@ -201,7 +263,7 @@ export class PersistentConnection extends ServerActions {
201263
}
202264
assert(
203265
query.getQueryParams().isDefault() ||
204-
!query.getQueryParams().loadsAllData(),
266+
!query.getQueryParams().loadsAllData(),
205267
'listen() called for non-default but complete query'
206268
);
207269
assert(
@@ -221,6 +283,24 @@ export class PersistentConnection extends ServerActions {
221283
}
222284
}
223285

286+
private sendGet_(index: number) {
287+
const get = this.outstandingGets_[index];
288+
this.sendRequest(
289+
get.action,
290+
get.request,
291+
(message: { [k: string]: unknown }) => {
292+
delete this.outstandingGets_[index];
293+
this.outstandingGetCount_--;
294+
if (this.outstandingGetCount_ === 0) {
295+
this.outstandingGets_ = [];
296+
}
297+
if (get.onComplete) {
298+
get.onComplete(message);
299+
}
300+
}
301+
);
302+
}
303+
224304
private sendListen_(listenSpec: ListenSpec) {
225305
const query = listenSpec.query;
226306
const pathString = query.path.toString();
@@ -273,8 +353,8 @@ export class PersistentConnection extends ServerActions {
273353
const indexPath = query.path.toString();
274354
warn(
275355
`Using an unspecified index. Your data will be downloaded and ` +
276-
`filtered on the client. Consider adding ${indexSpec} at ` +
277-
`${indexPath} to your security rules for better performance.`
356+
`filtered on the client. Consider adding ${indexSpec} at ` +
357+
`${indexPath} to your security rules for better performance.`
278358
);
279359
}
280360
}
@@ -292,7 +372,7 @@ export class PersistentConnection extends ServerActions {
292372
//If we're connected we want to let the server know to unauthenticate us. If we're not connected, simply delete
293373
//the credential so we dont become authenticated next time we connect.
294374
if (this.connected_) {
295-
this.sendRequest('unauth', {}, () => {});
375+
this.sendRequest('unauth', {}, () => { });
296376
}
297377
}
298378

@@ -356,7 +436,7 @@ export class PersistentConnection extends ServerActions {
356436

357437
assert(
358438
query.getQueryParams().isDefault() ||
359-
!query.getQueryParams().loadsAllData(),
439+
!query.getQueryParams().loadsAllData(),
360440
'unlisten() called for non-default but complete query'
361441
);
362442
const listen = this.removeListen_(pathString, queryId);
@@ -614,8 +694,8 @@ export class PersistentConnection extends ServerActions {
614694
} else {
615695
error(
616696
'Unrecognized action received from server: ' +
617-
stringify(action) +
618-
'\nAre you using the latest client?'
697+
stringify(action) +
698+
'\nAre you using the latest client?'
619699
);
620700
}
621701
}
@@ -950,6 +1030,12 @@ export class PersistentConnection extends ServerActions {
9501030
request.onComplete
9511031
);
9521032
}
1033+
1034+
for (let i = 0; i < this.outstandingGets_.length; i++) {
1035+
if (this.outstandingGets_[i]) {
1036+
this.sendGet_(i);
1037+
}
1038+
}
9531039
}
9541040

9551041
/**

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: 40 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,45 @@ export class Repo {
304305
return this.nextWriteId_++;
305306
}
306307

308+
getValue(query: Query): Promise<DataSnapshot> {
309+
return this.server_.get(query).then(
310+
payload => {
311+
const node = nodeFromJSON(payload as string);
312+
const events = this.serverSyncTree_.applyServerOverwrite(
313+
query.path,
314+
node
315+
);
316+
this.eventQueue_.raiseEventsAtPath(query.path, events);
317+
return Promise.resolve(
318+
new DataSnapshot(
319+
node,
320+
query.getRef(),
321+
query.getQueryParams().getIndex()
322+
)
323+
);
324+
},
325+
err => {
326+
this.log_(
327+
'get for query ' +
328+
stringify(query) +
329+
' falling back to cache after error: ' +
330+
err
331+
);
332+
const cached = this.serverSyncTree_.calcCompleteEventCache(query.path);
333+
if (!cached.isEmpty()) {
334+
return Promise.resolve(
335+
new DataSnapshot(
336+
cached,
337+
query.getRef(),
338+
query.getQueryParams().getIndex()
339+
)
340+
);
341+
}
342+
return Promise.reject(new Error(err as string));
343+
}
344+
);
345+
}
346+
307347
setWithPriority(
308348
path: Path,
309349
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)