Skip to content

Commit 542eb35

Browse files
author
Clément Le Provost
authored
New offline features (#185)
* [offline] Support bootstrapping mirrored indices with local data This feature allows initializing a local mirror from local data (typically, prepackaged resources) without having to wait for the first sync. * [offline] Expose `hasOfflineData()` at the index level * [offline] Add a `buildOffline(…)` method on `MirroredIndex` This unconditionally creates or replaces the local mirror with the specified data, regardless of whether local data already exists or not for the index. (In contrast to `boostrap(…)`, which only does so if no data exists.) * [offline] Support offline fallback for getting objects (WIP) The `Index.getObjects(…)` methods are overridden by `MirroredIndex`, providing the same offline fallback mechanism as search. NOTE: Only supported for `getObjects` so far; getting a single object will follow later. * [offline] Fix race condition in instantiation of local index It turns out that [lazy variables are not thread-safe](http://stackoverflow.com/a/29761777). We therefore have to synchronize initialization manually. * [offline] Refactor recent features - Remove `bootstrap(…)`: it is superseded by `buildOffline(…)` - Use a dynamic property instead of a function for `hasOfflineData` - Rename notifications from `bootstrapDid*` to `buildDid*` - Update reference documentation * [offline] Leverage `buildOffline` inside sync This allows to send build notifications also for a sync. * [offline] Add unit test for `MirroredIndex.buildOffline(…)` * Fix compilation warnings The `syncErrorKey` notification key is now deprecated in favor of `errorKey`. * [offline] Add utilities to tag content origin as local or remote * [offline] Add offline support to `MirroredIndex.getObject(…)` * [offline] Fix ambiguous overload * [refact] Use utilities to tag content origin * Ooops * [offline] Add test cases for `MirroredIndex.getObject[s]` * [offline] Fix compilation of unit tests Xcode or Cocoapods (not sure which one) has trouble dealing with two distinct modules both named “AlgoliaSearch”. We therefore rename the offline flavor to “AlgoliaSearchOffline”. NOTE: This is **not** what we want clients of the library to be using, but since the module name is overridden by Cocoapods, and the scheme is not shared with Carthage, this should impact only the unit tests, not the users of the module. * [refact] Add completion handler to `MirroredIndex.buildOffline(…)` * [refact] Use dynamic property instead of function for `OfflineIndex.hasOfflineData` * [offline] Add manual build capability to `OfflineIndex` * Fix some inconsistencies * [doc][offline] Update reference documentation [ci skip] * [doc][offline] Fix generation of reference documentation Changing the module name had unintended side effects on the documentation. This should fix it. * [refact][offline] Return an operation from manual build methods * [offline] Assert that mirroring is on before testing local data * [offline][doc] Update reference documentation [ci skip]
1 parent eaa9a50 commit 542eb35

File tree

14 files changed

+759
-109
lines changed

14 files changed

+759
-109
lines changed

AlgoliaSearch.xcodeproj/project.pbxproj

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,16 @@
139139
BCD57D231D89947500C5DE68 /* DisjunctiveFaceting.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD57D1E1D89947500C5DE68 /* DisjunctiveFaceting.swift */; };
140140
BCD57D261D89970D00C5DE68 /* MultipleQueryEmulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD57D241D89970D00C5DE68 /* MultipleQueryEmulator.swift */; };
141141
BCD57D271D89970D00C5DE68 /* OfflineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD57D251D89970D00C5DE68 /* OfflineIndex.swift */; };
142-
BCD57D311D89B12E00C5DE68 /* AlgoliaSearch.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC4BD2931D54E48900170ECC /* AlgoliaSearch.framework */; };
142+
BCD57D311D89B12E00C5DE68 /* AlgoliaSearchOffline.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC4BD2931D54E48900170ECC /* AlgoliaSearchOffline.framework */; };
143143
BCD57D3C1D89B1EA00C5DE68 /* OfflineClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD57D391D89B1EA00C5DE68 /* OfflineClientTests.swift */; };
144144
BCD57D3D1D89B1EA00C5DE68 /* OfflineIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD57D3A1D89B1EA00C5DE68 /* OfflineIndexTests.swift */; };
145145
BCD57D3E1D89B1EA00C5DE68 /* OfflineTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD57D3B1D89B1EA00C5DE68 /* OfflineTestCase.swift */; };
146146
BCDA73781CA546800082B197 /* NetworkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDA73771CA546800082B197 /* NetworkTests.swift */; };
147147
BCDA73791CA546800082B197 /* NetworkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDA73771CA546800082B197 /* NetworkTests.swift */; };
148148
BCDA737E1CA555070082B197 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDA737D1CA555070082B197 /* MockURLSession.swift */; };
149149
BCDA737F1CA555070082B197 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDA737D1CA555070082B197 /* MockURLSession.swift */; };
150+
BCEE36811E0830810091D113 /* settings.json in Resources */ = {isa = PBXBuildFile; fileRef = BCEE36801E0830810091D113 /* settings.json */; };
151+
BCEE36831E08309F0091D113 /* objects.json in Resources */ = {isa = PBXBuildFile; fileRef = BCEE36821E08309F0091D113 /* objects.json */; };
150152
BCFC281C1D92660C00BFE0A0 /* MirroredIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFC281B1D92660C00BFE0A0 /* MirroredIndexTests.swift */; };
151153
BCFC281D1D92670100BFE0A0 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D44D0261B362648008369AC /* Helpers.swift */; };
152154
/* End PBXBuildFile section */
@@ -215,7 +217,7 @@
215217
BC23A6EC1D63539A00DF9034 /* AlgoliaSearch.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AlgoliaSearch.framework; sourceTree = BUILT_PRODUCTS_DIR; };
216218
BC4A7F381CB5308100AF1DCB /* AsyncOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; };
217219
BC4A7F3B1CB5373E00AF1DCB /* CancelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancelTests.swift; sourceTree = "<group>"; };
218-
BC4BD2931D54E48900170ECC /* AlgoliaSearch.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AlgoliaSearch.framework; sourceTree = BUILT_PRODUCTS_DIR; };
220+
BC4BD2931D54E48900170ECC /* AlgoliaSearchOffline.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AlgoliaSearchOffline.framework; sourceTree = BUILT_PRODUCTS_DIR; };
219221
BC4DDEEF1DD10E8B004D9A6E /* AbstractClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AbstractClient.swift; sourceTree = "<group>"; };
220222
BC4DDEF51DD10EBA004D9A6E /* Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = "<group>"; };
221223
BC4DDEFB1DD11527004D9A6E /* PlacesClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlacesClient.swift; sourceTree = "<group>"; };
@@ -239,6 +241,8 @@
239241
BCD57D3B1D89B1EA00C5DE68 /* OfflineTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OfflineTestCase.swift; sourceTree = "<group>"; };
240242
BCDA73771CA546800082B197 /* NetworkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkTests.swift; sourceTree = "<group>"; };
241243
BCDA737D1CA555070082B197 /* MockURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = "<group>"; };
244+
BCEE36801E0830810091D113 /* settings.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = settings.json; sourceTree = "<group>"; };
245+
BCEE36821E08309F0091D113 /* objects.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = objects.json; sourceTree = "<group>"; };
242246
BCFC281B1D92660C00BFE0A0 /* MirroredIndexTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MirroredIndexTests.swift; sourceTree = "<group>"; };
243247
F52EEAD0E1031BA77193C0AF /* Pods_AlgoliaSearch_Offline_iOS_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AlgoliaSearch_Offline_iOS_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
244248
/* End PBXFileReference section */
@@ -308,7 +312,7 @@
308312
isa = PBXFrameworksBuildPhase;
309313
buildActionMask = 2147483647;
310314
files = (
311-
BCD57D311D89B12E00C5DE68 /* AlgoliaSearch.framework in Frameworks */,
315+
BCD57D311D89B12E00C5DE68 /* AlgoliaSearchOffline.framework in Frameworks */,
312316
3A11AC4EE771A46C306173F7 /* Pods_AlgoliaSearch_Offline_iOS_Tests.framework in Frameworks */,
313317
);
314318
runOnlyForDeploymentPostprocessing = 0;
@@ -345,7 +349,7 @@
345349
5DB251471AAD9EE400945339 /* AlgoliaSearch iOS Tests.xctest */,
346350
BCD1F54A1CC61C8D0006E227 /* AlgoliaSearch.framework */,
347351
BCD1F5531CC61C8D0006E227 /* AlgoliaSearch tvOS Tests.xctest */,
348-
BC4BD2931D54E48900170ECC /* AlgoliaSearch.framework */,
352+
BC4BD2931D54E48900170ECC /* AlgoliaSearchOffline.framework */,
349353
BC23A6EC1D63539A00DF9034 /* AlgoliaSearch.framework */,
350354
BCD57D2C1D89B12E00C5DE68 /* AlgoliaSearch-Offline-iOS-Tests.xctest */,
351355
);
@@ -456,6 +460,8 @@
456460
BCD57D3A1D89B1EA00C5DE68 /* OfflineIndexTests.swift */,
457461
BCD57D3B1D89B1EA00C5DE68 /* OfflineTestCase.swift */,
458462
BC6267ED1D8FE4180014C767 /* OfflineObjcBridging.m */,
463+
BCEE36801E0830810091D113 /* settings.json */,
464+
BCEE36821E08309F0091D113 /* objects.json */,
459465
);
460466
path = Offline;
461467
sourceTree = "<group>";
@@ -607,7 +613,7 @@
607613
);
608614
name = "AlgoliaSearch-Offline-iOS";
609615
productName = AlgoliaSarch;
610-
productReference = BC4BD2931D54E48900170ECC /* AlgoliaSearch.framework */;
616+
productReference = BC4BD2931D54E48900170ECC /* AlgoliaSearchOffline.framework */;
611617
productType = "com.apple.product-type.framework";
612618
};
613619
BCD1F5491CC61C8D0006E227 /* AlgoliaSearch tvOS */ = {
@@ -790,6 +796,8 @@
790796
isa = PBXResourcesBuildPhase;
791797
buildActionMask = 2147483647;
792798
files = (
799+
BCEE36831E08309F0091D113 /* objects.json in Resources */,
800+
BCEE36811E0830810091D113 /* settings.json in Resources */,
793801
);
794802
runOnlyForDeploymentPostprocessing = 0;
795803
};
@@ -1422,7 +1430,7 @@
14221430
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
14231431
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
14241432
PRODUCT_BUNDLE_IDENTIFIER = "com.algolia.$(PRODUCT_NAME:rfc1034identifier)";
1425-
PRODUCT_NAME = AlgoliaSearch;
1433+
PRODUCT_NAME = AlgoliaSearchOffline;
14261434
SDKROOT = iphoneos;
14271435
SKIP_INSTALL = YES;
14281436
TARGETED_DEVICE_FAMILY = "1,2";
@@ -1444,7 +1452,7 @@
14441452
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
14451453
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
14461454
PRODUCT_BUNDLE_IDENTIFIER = "com.algolia.$(PRODUCT_NAME:rfc1034identifier)";
1447-
PRODUCT_NAME = AlgoliaSearch;
1455+
PRODUCT_NAME = AlgoliaSearchOffline;
14481456
SDKROOT = iphoneos;
14491457
SKIP_INSTALL = YES;
14501458
TARGETED_DEVICE_FAMILY = "1,2";

Source/Index.swift

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -143,19 +143,19 @@ import Foundation
143143
///
144144
@objc
145145
@discardableResult public func getObject(withID objectID: String, completionHandler: @escaping CompletionHandler) -> Operation {
146-
let path = "1/indexes/\(urlEncodedName)/\(objectID.urlEncodedPathComponent())"
147-
return client.performHTTPQuery(path: path, method: .GET, body: nil, hostnames: client.readHosts, completionHandler: completionHandler)
146+
return getObject(withID: objectID, attributesToRetrieve: nil, completionHandler: completionHandler)
148147
}
149148

150149
/// Get an object from this index, optionally restricting the retrieved content.
151150
///
152151
/// - parameter objectID: Identifier of the object to retrieve.
153-
/// - parameter attributesToRetrieve: List of attributes to retrieve.
152+
/// - parameter attributesToRetrieve: List of attributes to retrieve. If `nil`, all attributes are retrieved.
153+
/// If one of the elements is `"*"`, all attributes are retrieved.
154154
/// - parameter completionHandler: Completion handler to be notified of the request's outcome.
155155
/// - returns: A cancellable operation.
156156
///
157157
@objc
158-
@discardableResult public func getObject(withID objectID: String, attributesToRetrieve: [String], completionHandler: @escaping CompletionHandler) -> Operation {
158+
@discardableResult public func getObject(withID objectID: String, attributesToRetrieve: [String]?, completionHandler: @escaping CompletionHandler) -> Operation {
159159
let query = Query()
160160
query.attributesToRetrieve = attributesToRetrieve
161161
let path = "1/indexes/\(urlEncodedName)/\(objectID.urlEncodedPathComponent())?\(query.build())"
@@ -170,16 +170,7 @@ import Foundation
170170
///
171171
@objc
172172
@discardableResult public func getObjects(withIDs objectIDs: [String], completionHandler: @escaping CompletionHandler) -> Operation {
173-
let path = "1/indexes/*/objects"
174-
175-
var requests = [Any]()
176-
requests.reserveCapacity(objectIDs.count)
177-
for id in objectIDs {
178-
requests.append(["indexName": self.name, "objectID": id])
179-
}
180-
let request = ["requests": requests]
181-
182-
return client.performHTTPQuery(path: path, method: .POST, body: request, hostnames: client.readHosts, completionHandler: completionHandler)
173+
return getObjects(withIDs: objectIDs, attributesToRetrieve: nil, completionHandler: completionHandler)
183174
}
184175

185176
/// Get several objects from this index, optionally restricting the retrieved content.
@@ -191,16 +182,16 @@ import Foundation
191182
/// - returns: A cancellable operation.
192183
///
193184
@objc
194-
@discardableResult public func getObjects(withIDs objectIDs: [String], attributesToRetrieve: [String], completionHandler: @escaping CompletionHandler) -> Operation {
185+
@discardableResult public func getObjects(withIDs objectIDs: [String], attributesToRetrieve: [String]?, completionHandler: @escaping CompletionHandler) -> Operation {
195186
let path = "1/indexes/*/objects"
196187
var requests = [Any]()
197188
requests.reserveCapacity(objectIDs.count)
198189
for id in objectIDs {
199-
let request = [
190+
var request = [
200191
"indexName": self.name,
201192
"objectID": id,
202-
"attributesToRetrieve": attributesToRetrieve.joined(separator: ",")
203193
]
194+
request["attributesToRetrieve"] = attributesToRetrieve?.joined(separator: ",")
204195
requests.append(request as Any)
205196
}
206197
return client.performHTTPQuery(path: path, method: .POST, body: ["requests": requests], hostnames: client.readHosts, completionHandler: completionHandler)

0 commit comments

Comments
 (0)