Skip to content

Commit 4d988cc

Browse files
Adding Multi-Resource support to the Firebase iOS SDK (firebase#278)
* Adding Multi-Resource support to the Firebase iOS SDK. This CL also makes RepoInfo hashable and simplifies RepoManager based on this.
1 parent 64ff4f3 commit 4d988cc

File tree

6 files changed

+186
-54
lines changed

6 files changed

+186
-54
lines changed

Example/Database/Tests/Integration/FIRDatabaseTests.m

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ @interface FIRDatabaseTests : FTestBase
3232
@end
3333

3434
static const NSInteger kFErrorCodeWriteCanceled = 3;
35+
static NSString *kFirebaseTestAltNamespace = @"https://foobar.firebaseio.com";
3536

3637
@implementation FIRDatabaseTests
3738

@@ -50,9 +51,52 @@ - (void) testDatabaseForApp {
5051
- (void) testDatabaseForAppWithInvalidURLs {
5152
XCTAssertThrows([self databaseForURL:nil]);
5253
XCTAssertThrows([self databaseForURL:@"not-a-url"]);
54+
XCTAssertThrows([self databaseForURL:@"http://fblocal.com"]);
5355
XCTAssertThrows([self databaseForURL:@"http://x.example.com/paths/are/bad"]);
5456
}
5557

58+
- (void)testDatabaseForAppWithURL {
59+
id app = [[FIRFakeApp alloc] initWithName:@"testDatabaseForAppWithURL" URL:kFirebaseTestAltNamespace];
60+
FIRDatabase *database = [FIRDatabase databaseForApp:app URL:@"http://foo.bar.com"];
61+
XCTAssertEqualObjects(@"https://foo.bar.com", [database reference].URL);
62+
}
63+
64+
- (void)testDatabaseForAppWithURLAndPort {
65+
id app = [[FIRFakeApp alloc] initWithName:@"testDatabaseForAppWithURLAndPort"
66+
URL:kFirebaseTestAltNamespace];
67+
FIRDatabase *database = [FIRDatabase databaseForApp:app URL:@"http://foo.bar.com:80"];
68+
XCTAssertEqualObjects(@"http://foo.bar.com:80", [database reference].URL);
69+
}
70+
71+
- (void)testDatabaseForAppWithHttpsURL {
72+
id app = [[FIRFakeApp alloc] initWithName:@"testDatabaseForAppWithHttpsURL"
73+
URL:kFirebaseTestAltNamespace];
74+
FIRDatabase *database = [FIRDatabase databaseForApp:app URL:@"https://foo.bar.com"];
75+
XCTAssertEqualObjects(@"https://foo.bar.com", [database reference].URL);
76+
}
77+
78+
- (void)testDifferentInstanceForAppWithURL {
79+
id app = [[FIRFakeApp alloc] initWithName:@"testDifferentInstanceForAppWithURL"
80+
URL:kFirebaseTestAltNamespace];
81+
FIRDatabase *database1 = [FIRDatabase databaseForApp:app URL:@"https://foo1.bar.com"];
82+
FIRDatabase *database2 = [FIRDatabase databaseForApp:app URL:@"https://foo1.bar.com/"];
83+
FIRDatabase *database3 = [FIRDatabase databaseForApp:app URL:@"https://foo2.bar.com"];
84+
XCTAssertEqual(database1, database2);
85+
XCTAssertNotEqual(database1, database3);
86+
}
87+
88+
- (void)testDatabaseForAppWithInvalidCustomURLs {
89+
id app = [[FIRFakeApp alloc] initWithName:@"testDatabaseForAppWithInvalidCustomURLs"
90+
URL:kFirebaseTestAltNamespace];
91+
#pragma clang diagnostic push
92+
#pragma clang diagnostic ignored "-Wnonnull"
93+
XCTAssertThrows([FIRDatabase databaseForApp:app URL:nil]);
94+
#pragma clang diagnostic pop
95+
XCTAssertThrows([FIRDatabase databaseForApp:app URL:@"not-a-url"]);
96+
XCTAssertThrows([FIRDatabase databaseForApp:app URL:@"http://fblocal.com"]);
97+
XCTAssertThrows([FIRDatabase databaseForApp:app URL:@"http://x.fblocal.com:9000/paths/are/bad"]);
98+
}
99+
56100
- (void) testDeleteDatabase {
57101
// Set up a custom FIRApp with a custom database based on it.
58102
FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:123abc"

Firebase/Database/Api/FIRDatabase.m

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ @interface FIRDatabase ()
3636

3737
@implementation FIRDatabase
3838

39+
/** A NSMutableDictionary of FirebaseApp name and FRepoInfo to FirebaseDatabase instance. */
40+
typedef NSMutableDictionary<NSString *, NSMutableDictionary<FRepoInfo *, FIRDatabase *> *> FIRDatabaseDictionary;
41+
3942
// The STR and STR_EXPAND macro allow a numeric version passed to he compiler driver
4043
// with a -D to be treated as a string instead of an invalid floating point value.
4144
#define STR(x) STR_EXPAND(x)
@@ -51,30 +54,34 @@ + (void)load {
5154
NSString *appName = note.userInfo[kFIRAppNameKey];
5255
if (appName == nil) { return; }
5356

54-
NSMutableDictionary *instances = [self instances];
57+
FIRDatabaseDictionary* instances = [self instances];
5558
@synchronized (instances) {
56-
FIRDatabase *deletedApp = instances[appName];
57-
if (deletedApp) {
59+
NSMutableDictionary<FRepoInfo *, FIRDatabase *> *databaseInstances = instances[appName];
60+
if (databaseInstances) {
5861
// Clean up the deleted instance in an effort to remove any resources still in use.
5962
// Note: Any leftover instances of this exact database will be invalid.
60-
[FRepoManager disposeRepos:deletedApp.config];
63+
for (FIRDatabase * database in [databaseInstances allValues]) {
64+
[FRepoManager disposeRepos:database.config];
65+
}
6166
[instances removeObjectForKey:appName];
6267
}
6368
}
6469
}];
6570
}
6671

6772
/**
68-
* A static NSMutableDictionary of FirebaseApp names to FirebaseDatabase instance. To ensure thread-
69-
* safety, it should only be accessed in databaseForApp, which is synchronized.
73+
* A static NSMutableDictionary of FirebaseApp name and FRepoInfo to
74+
* FirebaseDatabase instance. To ensure thread-safety, it should only be
75+
* accessed in databaseForApp:URL:, which is synchronized.
7076
*
7177
* TODO: This serves a duplicate purpose as RepoManager. We should clean up.
72-
* TODO: We should maybe be conscious of leaks and make this a weak map or similar
73-
* but we have a lot of work to do to allow FirebaseDatabase/Repo etc. to be GC'd.
78+
* TODO: We should maybe be conscious of leaks and make this a weak map or
79+
* similar but we have a lot of work to do to allow FirebaseDatabase/Repo etc.
80+
* to be GC'd.
7481
*/
75-
+ (NSMutableDictionary *)instances {
82+
+ (FIRDatabaseDictionary *)instances {
7683
static dispatch_once_t pred = 0;
77-
static NSMutableDictionary *instances;
84+
static FIRDatabaseDictionary *instances;
7885
dispatch_once(&pred, ^{
7986
instances = [NSMutableDictionary dictionary];
8087
});
@@ -92,27 +99,59 @@ + (FIRDatabase *)database {
9299
return [FIRDatabase databaseForApp:app];
93100
}
94101

102+
+ (FIRDatabase *)databaseWithURL:(NSString *)url {
103+
FIRApp *app = [FIRApp defaultApp];
104+
if (app == nil) {
105+
[NSException raise:@"FIRAppNotConfigured"
106+
format:@"Failed to get default Firebase Database instance. "
107+
@"Must call `[FIRApp configure]` (`FirebaseApp.configure()` in Swift) "
108+
@"before using Firebase Database."];
109+
}
110+
return [FIRDatabase databaseForApp:app URL:url];
111+
}
112+
95113
+ (FIRDatabase *)databaseForApp:(FIRApp *)app {
96114
if (app == nil) {
97115
[NSException raise:@"InvalidFIRApp" format:@"nil FIRApp instance passed to databaseForApp."];
98116
}
99-
NSMutableDictionary *instances = [self instances];
100-
@synchronized (instances) {
101-
FIRDatabase *database = instances[app.name];
102-
if (!database) {
103-
NSString *databaseUrl = app.options.databaseURL;
104-
if (databaseUrl == nil) {
105-
[NSException raise:@"MissingDatabaseURL" format:@"Failed to get FIRDatabase instance: FIRApp object has no "
106-
"databaseURL in its FirebaseOptions object."];
107-
}
108117

109-
FParsedUrl *parsedUrl = [FUtilities parseUrl:databaseUrl];
110-
if (![parsedUrl.path isEmpty]) {
111-
[NSException raise:@"InvalidDatabaseURL" format:@"Configured Database URL '%@' is invalid. It should "
112-
"point to the root of a Firebase Database but it includes a path: %@",
113-
databaseUrl, [parsedUrl.path toString]];
114-
}
118+
return [FIRDatabase databaseForApp:app URL:app.options.databaseURL];
119+
}
115120

121+
+ (FIRDatabase *)databaseForApp:(FIRApp *)app URL:(NSString *)url {
122+
if (app == nil) {
123+
[NSException raise:@"InvalidFIRApp"
124+
format:@"nil FIRApp instance passed to databaseForApp."];
125+
}
126+
127+
if (url == nil) {
128+
[NSException raise:@"MissingDatabaseURL"
129+
format:@"Failed to get FirebaseDatabase instance: "
130+
"Specify DatabaseURL within FIRApp or from your databaseForApp:URL: call."];
131+
}
132+
133+
NSURL *databaseUrl = [NSURL URLWithString:url];
134+
135+
if (databaseUrl == nil) {
136+
[NSException raise:@"InvalidDatabaseURL" format:@"The Database URL '%@' cannot be parsed. "
137+
"Specify a valid DatabaseURL within FIRApp or from your databaseForApp:URL: call.", databaseUrl];
138+
} else if (![databaseUrl.path isEqualToString:@""] && ![databaseUrl.path isEqualToString:@"/"]) {
139+
[NSException raise:@"InvalidDatabaseURL" format:@"Configured Database URL '%@' is invalid. It should point "
140+
"to the root of a Firebase Database but it includes a path: %@",databaseUrl, databaseUrl.path];
141+
}
142+
143+
FIRDatabaseDictionary *instances = [self instances];
144+
@synchronized (instances) {
145+
NSMutableDictionary<FRepoInfo *, FIRDatabase *> *urlInstanceMap =
146+
instances[app.name];
147+
if (!urlInstanceMap) {
148+
urlInstanceMap = [NSMutableDictionary dictionary];
149+
instances[app.name] = urlInstanceMap;
150+
}
151+
152+
FParsedUrl *parsedUrl = [FUtilities parseUrl:databaseUrl.absoluteString];
153+
FIRDatabase *database = urlInstanceMap[parsedUrl.repoInfo];
154+
if (!database) {
116155
id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:app];
117156

118157
// If this is the default app, don't set the session persistence key so that we use our
@@ -125,8 +164,10 @@ + (FIRDatabase *)databaseForApp:(FIRApp *)app {
125164

126165
FIRDatabaseConfig *config = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:sessionIdentifier
127166
authTokenProvider:authTokenProvider];
128-
database = [[FIRDatabase alloc] initWithApp:app repoInfo:parsedUrl.repoInfo config:config];
129-
instances[app.name] = database;
167+
database = [[FIRDatabase alloc] initWithApp:app
168+
repoInfo:parsedUrl.repoInfo
169+
config:config];
170+
urlInstanceMap[parsedUrl.repoInfo] = database;
130171
}
131172

132173
return database;

Firebase/Database/Core/FRepoInfo.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
#import <Foundation/Foundation.h>
1818

19-
@interface FRepoInfo : NSObject
19+
@interface FRepoInfo : NSObject <NSCopying>
2020

2121
@property (nonatomic, readonly, strong) NSString* host;
2222
@property (nonatomic, readonly, strong) NSString* namespace;
@@ -31,4 +31,8 @@
3131
- (BOOL) isDemoHost;
3232
- (BOOL) isCustomHost;
3333

34+
- (id)copyWithZone:(NSZone *)zone;
35+
- (NSUInteger)hash;
36+
- (BOOL)isEqual:(id)anObject;
37+
3438
@end

Firebase/Database/Core/FRepoInfo.m

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,23 @@ - (NSString *) connectionURLWithLastSessionID:(NSString*)lastSessionID {
112112
return url;
113113
}
114114

115+
- (id)copyWithZone:(NSZone *)zone; {
116+
return self; // Immutable
117+
}
118+
119+
- (NSUInteger)hash {
120+
NSUInteger result = host.hash;
121+
result = 31 * result + (secure ? 1 : 0);
122+
result = 31 * result + namespace.hash;
123+
result = 31 * result + host.hash;
124+
return result;
125+
}
126+
127+
- (BOOL)isEqual:(id)anObject {
128+
if (![anObject isKindOfClass:[FRepoInfo class]]) return NO;
129+
FRepoInfo *other = (FRepoInfo *)anObject;
130+
return secure == other.secure && [host isEqualToString:other.host] &&
131+
[namespace isEqualToString:other.namespace];
132+
}
133+
115134
@end

Firebase/Database/Core/FRepoManager.m

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@
2424

2525
@implementation FRepoManager
2626

27-
+ (NSMutableDictionary *)configs {
27+
typedef NSMutableDictionary<NSString *,
28+
NSMutableDictionary<FRepoInfo *, FRepo *> *>
29+
FRepoDictionary;
30+
31+
+ (FRepoDictionary *)configs {
2832
static dispatch_once_t pred = 0;
29-
static NSMutableDictionary *configs;
33+
static FRepoDictionary *configs;
3034
dispatch_once(&pred, ^{
3135
configs = [NSMutableDictionary dictionary];
3236
});
@@ -39,33 +43,32 @@ + (NSMutableDictionary *)configs {
3943
*/
4044
+ (FRepo *) getRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config {
4145
[config freeze];
42-
NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", repoInfo.host, repoInfo.namespace];
43-
NSMutableDictionary *configs = [FRepoManager configs];
46+
FRepoDictionary *configs = [FRepoManager configs];
4447
@synchronized(configs) {
45-
NSMutableDictionary *repos = configs[config.sessionIdentifier];
46-
if (!repos || repos[repoHashString] == nil) {
48+
NSMutableDictionary<FRepoInfo *, FRepo *> *repos = configs[config.sessionIdentifier];
49+
if (!repos || repos[repoInfo] == nil) {
4750
// Calling this should create the repo.
4851
[FIRDatabase createDatabaseForTests:repoInfo config:config];
4952
}
5053

51-
return configs[config.sessionIdentifier][repoHashString];
54+
return configs[config.sessionIdentifier][repoInfo];
5255
}
5356
}
5457

5558
+ (FRepo *) createRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database {
5659
[config freeze];
57-
NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", repoInfo.host, repoInfo.namespace];
58-
NSMutableDictionary *configs = [FRepoManager configs];
60+
FRepoDictionary *configs = [FRepoManager configs];
5961
@synchronized(configs) {
60-
NSMutableDictionary *repos = configs[config.sessionIdentifier];
62+
NSMutableDictionary<FRepoInfo *, FRepo *> *repos =
63+
configs[config.sessionIdentifier];
6164
if (!repos) {
6265
repos = [NSMutableDictionary dictionary];
6366
configs[config.sessionIdentifier] = repos;
6467
}
65-
FRepo *repo = repos[repoHashString];
68+
FRepo *repo = repos[repoInfo];
6669
if (repo == nil) {
6770
repo = [[FRepo alloc] initWithRepoInfo:repoInfo config:config database:database];
68-
repos[repoHashString] = repo;
71+
repos[repoInfo] = repo;
6972
return repo;
7073
} else {
7174
[NSException raise:@"RepoExists" format:@"createRepo called for Repo that already exists."];
@@ -76,40 +79,40 @@ + (FRepo *) createRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config
7679

7780
+ (void) interrupt:(FIRDatabaseConfig *)config {
7881
dispatch_async([FIRDatabaseQuery sharedQueue], ^{
79-
NSMutableDictionary *configs = [FRepoManager configs];
80-
NSMutableDictionary *repos = configs[config.sessionIdentifier];
81-
for (FRepo* repo in [repos allValues]) {
82+
FRepoDictionary *configs = [FRepoManager configs];
83+
NSMutableDictionary<FRepoInfo *, FRepo *> *repos = configs[config.sessionIdentifier];
84+
for (FRepo *repo in [repos allValues]) {
8285
[repo interrupt];
8386
}
8487
});
8588
}
8689

8790
+ (void) interruptAll {
8891
dispatch_async([FIRDatabaseQuery sharedQueue], ^{
89-
NSMutableDictionary *configs = [FRepoManager configs];
90-
for (NSMutableDictionary *repos in [configs allValues]) {
91-
for (FRepo* repo in [repos allValues]) {
92-
[repo interrupt];
93-
}
92+
FRepoDictionary *configs = [FRepoManager configs];
93+
for (NSMutableDictionary<FRepoInfo *, FRepo *> *repos in [configs allValues]) {
94+
for (FRepo *repo in [repos allValues]) {
95+
[repo interrupt];
96+
}
9497
}
9598
});
9699
}
97100

98101
+ (void) resume:(FIRDatabaseConfig *)config {
99102
dispatch_async([FIRDatabaseQuery sharedQueue], ^{
100-
NSMutableDictionary *configs = [FRepoManager configs];
101-
NSMutableDictionary *repos = configs[config.sessionIdentifier];
102-
for (FRepo* repo in [repos allValues]) {
103+
FRepoDictionary *configs = [FRepoManager configs];
104+
NSMutableDictionary<FRepoInfo *, FRepo *> *repos = configs[config.sessionIdentifier];
105+
for (FRepo *repo in [repos allValues]) {
103106
[repo resume];
104107
}
105108
});
106109
}
107110

108111
+ (void) resumeAll {
109112
dispatch_async([FIRDatabaseQuery sharedQueue], ^{
110-
NSMutableDictionary *configs = [FRepoManager configs];
111-
for (NSMutableDictionary *repos in [configs allValues]) {
112-
for (FRepo* repo in [repos allValues]) {
113+
FRepoDictionary *configs = [FRepoManager configs];
114+
for (NSMutableDictionary<FRepoInfo *, FRepo *> *repos in [configs allValues]) {
115+
for (FRepo *repo in [repos allValues]) {
113116
[repo resume];
114117
}
115118
}

Firebase/Database/Public/FIRDatabase.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ FIR_SWIFT_NAME(Database)
4545
*/
4646
+ (FIRDatabase *) database FIR_SWIFT_NAME(database());
4747

48+
/**
49+
* Gets a FirebaseDatabase instance for the specified URL.
50+
*
51+
* @param url The URL to the Firebase Database instance you want to access.
52+
* @return A FIRDatabase instance.
53+
*/
54+
+ (FIRDatabase *)databaseWithURL:(NSString *)url NS_SWIFT_NAME(database(url:));
55+
56+
/**
57+
* Gets a FirebaseDatabase instance for the specified URL, using the specified
58+
* FirebaseApp.
59+
*
60+
* @param app The FIRApp to get a FIRDatabase for.
61+
* @param url The URL to the Firebase Database instance you want to access.
62+
* @return A FIRDatabase instance.
63+
*/
64+
// clang-format off
65+
+ (FIRDatabase *)databaseForApp:(FIRApp *)app
66+
URL:(NSString *)url NS_SWIFT_NAME(database(app:url:));
67+
// clang-format on
68+
4869
/**
4970
* Gets an instance of FIRDatabase for a specific FIRApp.
5071
*

0 commit comments

Comments
 (0)