Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Ably.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions Source/ARTLocalDeviceFetcher.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#import "ARTLocalDeviceFetcher.h"
#import "ARTLocalDeviceFetcher+Testing.h"
#import "ARTLocalDevice+Private.h"

@interface ARTDefaultLocalDeviceFetcher ()

@property (nonatomic, nullable) ARTLocalDevice *device;
@property (nonatomic, readonly) dispatch_semaphore_t semaphore;

@end

@implementation ARTDefaultLocalDeviceFetcher

- (instancetype)init {
if (self = [super init]) {
_semaphore = dispatch_semaphore_create(1);
}

return self;
}

// The device is shared in a static variable because it's a reflection
// of what's persisted. Having a device instance per ARTRest instance
// could leave some instances in a stale state, if, through another
// instance, the persisted state is changed.
//
// As a side effect, the first ARTRest instance "wins" at setting the device's
// client ID.
+ (ARTDefaultLocalDeviceFetcher *)sharedInstance {
static ARTDefaultLocalDeviceFetcher *sharedInstance;
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
sharedInstance = [[ARTDefaultLocalDeviceFetcher alloc] init];
});

return sharedInstance;
}

- (ARTLocalDevice *)fetchLocalDeviceWithClientID:(NSString *)clientID storage:(id<ARTDeviceStorage>)storage logger:(ARTInternalLog *)logger {
NSLog(@"fetchLocalDeviceWithClientID on default called");
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
if (!self.device) {
self.device = [ARTLocalDevice load:clientID storage:storage logger:logger];
}
ARTLocalDevice *const device = self.device;
dispatch_semaphore_signal(self.semaphore);

return device;
}

- (void)resetDevice {
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
self.device = nil;
dispatch_semaphore_signal(self.semaphore);
}

@end
48 changes: 6 additions & 42 deletions Source/ARTRest.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#import "ARTLogAdapter.h"
#import "ARTClientOptions+TestConfiguration.h"
#import "ARTTestClientOptions.h"
#import "ARTLocalDeviceFetcher.h"

@implementation ARTRest {
ARTQueuedDealloc *_dealloc;
Expand Down Expand Up @@ -150,6 +151,7 @@ - (ARTLocalDevice *)device_nosync {
@interface ARTRestInternal ()

@property (nonatomic, readonly) ARTInternalLog *logger;
@property (nonatomic, readonly) id<ARTLocalDeviceFetcher> localDeviceFetcher;

@end

Expand All @@ -175,6 +177,7 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options realtime:(ARTRealtim
_realtime = realtime;
_options = [options copy];
_logger = logger;
_localDeviceFetcher = options.testOptions.localDeviceFetcher;
_queue = options.internalDispatchQueue;
_userQueue = options.dispatchQueue;
#if TARGET_OS_IOS
Expand Down Expand Up @@ -735,48 +738,9 @@ - (ARTLocalDevice *)device {
}

- (ARTLocalDevice *)device_nosync {
NSString *clientId = self.auth.clientId_nosync;
__block ARTLocalDevice *ret;
dispatch_sync(ARTRestInternal.deviceAccessQueue, ^{
ret = [self deviceWithClientId_onlyCallOnDeviceAccessQueue:clientId];
});
return ret;
}

+ (dispatch_queue_t)deviceAccessQueue {
static dispatch_queue_t queue;
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("io.ably.deviceAccess", DISPATCH_QUEUE_SERIAL);
});

return queue;
}

static BOOL sharedDeviceNeedsLoading_onlyAccessOnDeviceAccessQueue = YES;

- (ARTLocalDevice *)deviceWithClientId_onlyCallOnDeviceAccessQueue:(NSString *)clientId {
// The device is shared in a static variable because it's a reflection
// of what's persisted. Having a device instance per ARTRest instance
// could leave some instances in a stale state, if, through another
// instance, the persisted state is changed.
//
// As a side effect, the first instance "wins" at setting the device's
// client ID.

static id device;
if (sharedDeviceNeedsLoading_onlyAccessOnDeviceAccessQueue) {
device = [ARTLocalDevice load:clientId storage:self.storage logger:self.logger];
sharedDeviceNeedsLoading_onlyAccessOnDeviceAccessQueue = NO;
}
return device;
}

- (void)resetDeviceSingleton {
dispatch_sync([ARTRestInternal deviceAccessQueue], ^{
sharedDeviceNeedsLoading_onlyAccessOnDeviceAccessQueue = YES;
});
return [self.localDeviceFetcher fetchLocalDeviceWithClientID:self.auth.clientId_nosync
storage:self.storage
logger:self.logger];
}
#endif

Expand Down
3 changes: 3 additions & 0 deletions Source/ARTTestClientOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#import "ARTDefault.h"
#import "ARTFallback+Private.h"
#import "ARTRealtimeTransportFactory.h"
#import "ARTLocalDeviceFetcher.h"

@implementation ARTTestClientOptions

Expand All @@ -10,6 +11,7 @@ - (instancetype)init {
_realtimeRequestTimeout = [ARTDefault realtimeRequestTimeout];
_shuffleArray = ARTFallback_shuffleArray;
_transportFactory = [[ARTDefaultRealtimeTransportFactory alloc] init];
_localDeviceFetcher = ARTDefaultLocalDeviceFetcher.sharedInstance;
}

return self;
Expand All @@ -21,6 +23,7 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone {
copied.realtimeRequestTimeout = self.realtimeRequestTimeout;
copied.shuffleArray = self.shuffleArray;
copied.transportFactory = self.transportFactory;
copied.localDeviceFetcher = self.localDeviceFetcher;

return copied;
}
Expand Down
2 changes: 2 additions & 0 deletions Source/Ably.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,7 @@ framework module Ably {
header "ARTInternalLogCore+Testing.h"
header "ARTDataEncoder.h"
header "ARTRealtimeTransportFactory.h"
header "ARTLocalDeviceFetcher.h"
header "ARTLocalDeviceFetcher+Testing.h"
}
}
2 changes: 1 addition & 1 deletion Source/PrivateHeaders/Ably/ARTLocalDevice+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extern NSString *const ARTAPNSDeviceTokenKey;

@property (nonatomic) id<ARTDeviceStorage> storage;

+ (ARTLocalDevice *)load:(NSString *)clientId storage:(id<ARTDeviceStorage>)storage logger:(nullable ARTInternalLog *)logger;
+ (ARTLocalDevice *)load:(nullable NSString *)clientId storage:(id<ARTDeviceStorage>)storage logger:(nullable ARTInternalLog *)logger;
- (nullable NSString *)apnsDeviceToken;
- (void)setAndPersistAPNSDeviceToken:(nullable NSString *)deviceToken;
- (void)setAndPersistIdentityTokenDetails:(nullable ARTDeviceIdentityTokenDetails *)tokenDetails;
Expand Down
16 changes: 16 additions & 0 deletions Source/PrivateHeaders/Ably/ARTLocalDeviceFetcher+Testing.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@import Foundation;

NS_ASSUME_NONNULL_BEGIN

@interface ARTDefaultLocalDeviceFetcher ()

/**
Clears the fetcher’s internal reference to the device object, such that the next time it receives a `-fetchLocalDeviceWithClientID:storage:logger:` message, it initializes a new `ARTLocalDevice` instance.

This method should only be used in test code.
*/
- (void)resetDevice;

@end

NS_ASSUME_NONNULL_END
41 changes: 41 additions & 0 deletions Source/PrivateHeaders/Ably/ARTLocalDeviceFetcher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@import Foundation;

@protocol ARTDeviceStorage;
@class ARTLocalDevice;
@class ARTInternalLog;

NS_ASSUME_NONNULL_BEGIN

/**
`ARTLocalDeviceFetcher` is responsible for fetching an instance of `ARTLocalDevice`.
*/
NS_SWIFT_NAME(LocalDeviceFetcher)
@protocol ARTLocalDeviceFetcher

/**
Fetches an `ARTLocalDevice` instance. The receiver may ignore the arguments (e.g. if it has already instantiated an `ARTLocalDevice` instance, it may instead return that instance).

This method can safely be called from any thread.
*/
- (ARTLocalDevice *)fetchLocalDeviceWithClientID:(nullable NSString *)clientID
storage:(id<ARTDeviceStorage>)storage
logger:(nullable ARTInternalLog *)logger;

@end

/**
The implementation of `ARTLocalDeviceFetcher` that should be used in non-test code. Its `sharedInstance` class property manages the `ARTLocalDevice` instance shared by all `ARTRest` instances.
*/
NS_SWIFT_NAME(DefaultLocalDeviceFetcher)
@interface ARTDefaultLocalDeviceFetcher: NSObject <ARTLocalDeviceFetcher>

/**
Use `ARTDefaultLocalDeviceFetcher.sharedInstance` instead of `init`.
*/
- (instancetype)init NS_UNAVAILABLE;

@property (nonatomic, class, readonly) ARTDefaultLocalDeviceFetcher *sharedInstance;

@end

NS_ASSUME_NONNULL_END
5 changes: 0 additions & 5 deletions Source/PrivateHeaders/Ably/ARTRest+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,6 @@ NS_ASSUME_NONNULL_BEGIN

- (nullable NSObject<ARTCancellable> *)internetIsUp:(void (^)(BOOL isUp))cb;

#if TARGET_OS_IOS
// This is only intended to be called from test code.
- (void)resetDeviceSingleton;
#endif

@end

@interface ARTRest ()
Expand Down
6 changes: 6 additions & 0 deletions Source/PrivateHeaders/Ably/ARTTestClientOptions.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@import Foundation;

@protocol ARTRealtimeTransportFactory;
@protocol ARTLocalDeviceFetcher;

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -31,6 +32,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic) id<ARTRealtimeTransportFactory> transportFactory;

/**
Initial value is `ARTDefaultLocalDeviceFetcher.sharedInstance`.
*/
@property (nonatomic) id<ARTLocalDeviceFetcher> localDeviceFetcher;

@end

NS_ASSUME_NONNULL_END
2 changes: 2 additions & 0 deletions Source/include/Ably.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,7 @@ framework module Ably {
header "Ably/ARTInternalLogCore+Testing.h"
header "Ably/ARTDataEncoder.h"
header "Ably/ARTRealtimeTransportFactory.h"
header "Ably/ARTLocalDeviceFetcher.h"
header "Ably/ARTLocalDeviceFetcher+Testing.h"
}
}
20 changes: 20 additions & 0 deletions Test/Test Utilities/MockLocalDeviceFetcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Ably.Private

@objc(ARTMockLocalDeviceFetcher)
class MockLocalDeviceFetcher: NSObject, LocalDeviceFetcher {
private let semaphore = DispatchSemaphore(value: 1)
private var device: ARTLocalDevice?

func fetchLocalDevice(withClientID clientID: String?, storage: ARTDeviceStorage, logger: InternalLog?) -> ARTLocalDevice {
semaphore.wait()
let device: ARTLocalDevice
if let existingDevice = self.device {
device = existingDevice
} else {
device = ARTLocalDevice.load(clientID, storage: storage, logger: logger)
self.device = device
}
semaphore.signal()
return device
}
}
38 changes: 13 additions & 25 deletions Test/Test Utilities/TestUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,6 @@ class AblyTests {
checkError(errorInfo, withAlternative: "")
}

class var jsonRestOptions: ARTClientOptions {
get {
let options = AblyTests.clientOptions()
return options
}
}

static var testApplication: JSON?

struct QueueIdentity {
Expand All @@ -123,14 +116,18 @@ class AblyTests {
return DispatchQueue.getSpecific(key: queueIdentityKey)?.label
}

class func setupOptions(_ options: ARTClientOptions, forceNewApp: Bool = false, debug: Bool = false) -> ARTClientOptions {
class func commonAppSetup(debug: Bool = false, forceNewApp: Bool = false) -> ARTClientOptions {
let options = AblyTests.clientOptions(debug: debug)
options.testOptions.channelNamePrefix = "test-\(UUID().uuidString)"

if forceNewApp {
testApplication = nil
}

guard let app = testApplication else {
let app: JSON
if let testApplication {
app = testApplication
} else {
let request = NSMutableURLRequest(url: URL(string: "https://\(options.restHost):\(options.tlsPort)/apps")!)
request.httpMethod = "POST"
request.httpBody = try? appSetupJson["post_apps"].rawData()
Expand All @@ -146,35 +143,25 @@ class AblyTests {
fatalError(error.localizedDescription)
}

testApplication = try! JSON(data: responseData!)
app = try! JSON(data: responseData!)
testApplication = app

if debug {
print(testApplication!)
print(app)
}

return setupOptions(options, debug: debug)
}

let key = app["keys"][0]
options.key = key["keyStr"].stringValue
options.dispatchQueue = DispatchQueue.main
options.internalDispatchQueue = queue
if debug {
options.logLevel = .verbose
}

return options
}

class func commonAppSetup(_ debug: Bool = false) -> ARTClientOptions {
return AblyTests.setupOptions(AblyTests.jsonRestOptions, debug: debug)
}

class func clientOptions(_ debug: Bool = false, key: String? = nil, requestToken: Bool = false) -> ARTClientOptions {
class func clientOptions(debug: Bool = false, key: String? = nil, requestToken: Bool = false) -> ARTClientOptions {
let options = ARTClientOptions()
options.environment = getEnvironment()
options.logExceptionReportingUrl = nil
if debug {
options.logLevel = .debug
options.logLevel = .verbose
}
if let key = key {
options.key = key
Expand All @@ -184,6 +171,7 @@ class AblyTests {
}
options.dispatchQueue = DispatchQueue.main
options.internalDispatchQueue = queue
options.testOptions.localDeviceFetcher = MockLocalDeviceFetcher()
return options
}

Expand Down
Loading