Skip to content

Commit

Permalink
Add an example of a plugins mechanism
Browse files Browse the repository at this point in the history
Part of #2035, investigating how we might implement the upcoming
LiveObjects SDK as a plugin to the core SDK.

The code here is consumed by the following proof of concepts:

- LiveObjects plugin [1] - demonstrates how to consume the APIs that
  this commit exposes
- LiveObjects example [2] - demonstrates how a user would use the
  LiveObjects plugin

[1] /~https://github.com/lawrence-forooghian/ably-cocoa-liveobjects-plugin
[2] /~https://github.com/lawrence-forooghian/ably-cocoa-liveobjects-example
  • Loading branch information
lawrence-forooghian committed Feb 25, 2025
1 parent 0da2fec commit c624de3
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 6 deletions.
6 changes: 6 additions & 0 deletions Ably.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Pod::Spec.new do |s|
s.resource_bundles = {'Ably' => ['Source/PrivacyInfo.xcprivacy']}
s.private_header_files = 'Source/PrivateHeaders/**/*.h', 'Source/SocketRocket/**/*.h'
s.module_map = 'Source/Ably.modulemap'
# This is for the import of `APLivePlugin.h` to resolve. Copied from:
# - https://stackoverflow.com/questions/28425765/add-a-user-header-search-path-to-a-podspec
# - https://stackoverflow.com/questions/58690010/including-a-directory-as-a-search-path-during-compilation-with-cocoapods
s.pod_target_xcconfig = {
'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}/AblyPlugin/include"'
}
s.dependency 'msgpack', '0.4.0'
s.dependency 'AblyDeltaCodec', '1.3.3'
end
38 changes: 32 additions & 6 deletions Ably.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@
21AC0CD52D4AA3200030BD23 /* ARTWrapperSDKProxyRealtimeChannels.m in Sources */ = {isa = PBXBuildFile; fileRef = 21AC0CD12D4AA3200030BD23 /* ARTWrapperSDKProxyRealtimeChannels.m */; };
21AC0CD62D4AA3200030BD23 /* ARTWrapperSDKProxyRealtimeChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = 21AC0CD02D4AA3200030BD23 /* ARTWrapperSDKProxyRealtimeChannel.m */; };
21AC0CD72D4AA3200030BD23 /* ARTWrapperSDKProxyRealtimeChannels.m in Sources */ = {isa = PBXBuildFile; fileRef = 21AC0CD12D4AA3200030BD23 /* ARTWrapperSDKProxyRealtimeChannels.m */; };
21BFC2D62D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h in Headers */ = {isa = PBXBuildFile; fileRef = 21BFC2D52D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h */; settings = {ATTRIBUTES = (Private, ); }; };
21BFC2D72D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h in Headers */ = {isa = PBXBuildFile; fileRef = 21BFC2D52D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h */; settings = {ATTRIBUTES = (Private, ); }; };
21BFC2D82D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h in Headers */ = {isa = PBXBuildFile; fileRef = 21BFC2D52D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h */; settings = {ATTRIBUTES = (Private, ); }; };
21E1C0E52A0DC47400A5DB65 /* ARTWebSocketFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 21E1C0E42A0DC47400A5DB65 /* ARTWebSocketFactory.h */; settings = {ATTRIBUTES = (Private, ); }; };
21E1C0E62A0DC47400A5DB65 /* ARTWebSocketFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 21E1C0E42A0DC47400A5DB65 /* ARTWebSocketFactory.h */; settings = {ATTRIBUTES = (Private, ); }; };
21E1C0E72A0DC47400A5DB65 /* ARTWebSocketFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 21E1C0E42A0DC47400A5DB65 /* ARTWebSocketFactory.h */; settings = {ATTRIBUTES = (Private, ); }; };
Expand Down Expand Up @@ -1377,6 +1380,7 @@
21AC0CC92D4AA0F50030BD23 /* ARTWrapperSDKProxyRealtimeChannels+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "ARTWrapperSDKProxyRealtimeChannels+Private.h"; path = "PrivateHeaders/Ably/ARTWrapperSDKProxyRealtimeChannels+Private.h"; sourceTree = "<group>"; };
21AC0CD02D4AA3200030BD23 /* ARTWrapperSDKProxyRealtimeChannel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTWrapperSDKProxyRealtimeChannel.m; sourceTree = "<group>"; };
21AC0CD12D4AA3200030BD23 /* ARTWrapperSDKProxyRealtimeChannels.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTWrapperSDKProxyRealtimeChannels.m; sourceTree = "<group>"; };
21BFC2D52D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "ARTRealtimeChannel+Plugins.h"; path = "PrivateHeaders/Ably/ARTRealtimeChannel+Plugins.h"; sourceTree = "<group>"; };
21DCDA8229F818630073A211 /* Ably-iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "Ably-iOS.xctestplan"; path = "Test/Ably-iOS.xctestplan"; sourceTree = SOURCE_ROOT; };
21DCDA8329F81B350073A211 /* Ably-macOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Ably-macOS.xctestplan"; sourceTree = "<group>"; };
21DCDA8429F81B550073A211 /* Ably-tvOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Ably-tvOS.xctestplan"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2124,6 +2128,7 @@
211A60DE29D7272000D169C5 /* ARTConnectionStateChangeParams.m */,
D746AE3A1BBC5AE1003ECEF8 /* ARTRealtimeChannel.h */,
D746AE421BBC5CD0003ECEF8 /* ARTRealtimeChannel+Private.h */,
21BFC2D52D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h */,
D746AE3B1BBC5AE1003ECEF8 /* ARTRealtimeChannel.m */,
211A60FA29D8ABCF00D169C5 /* ARTChannelStateChangeParams.h */,
211A60FE29D8ABF100D169C5 /* ARTChannelStateChangeParams.m */,
Expand Down Expand Up @@ -2547,6 +2552,7 @@
D746AE1E1BBB5207003ECEF8 /* ARTDataQuery+Private.h in Headers */,
2132C2BD29D482E8000C4355 /* ARTClientOptions+TestConfiguration.h in Headers */,
D7AE18C91E5B40C900478D82 /* ARTPushAdmin.h in Headers */,
21BFC2D72D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h in Headers */,
EB1B53FD22F8D91C006A59AC /* ARTQueuedDealloc.h in Headers */,
D77394031C6F6FFE00F5478F /* ARTProtocolMessage+Private.h in Headers */,
D746AE2F1BBBE7D7003ECEF8 /* ARTPaginatedResult+Private.h in Headers */,
Expand Down Expand Up @@ -2682,6 +2688,7 @@
D710D69221949EFF008F54AD /* ARTJsonEncoder.h in Headers */,
21113B4629DB484200652C86 /* ARTChannel+Subclass.h in Headers */,
D710D5B921949D4F008F54AD /* ARTTokenParams+Private.h in Headers */,
21BFC2D82D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h in Headers */,
D710D51821949C42008F54AD /* ARTPushChannelSubscription.h in Headers */,
D710D4B421949B47008F54AD /* ARTAuth+Private.h in Headers */,
D710D4C221949B9C008F54AD /* ARTRealtimeTransport.h in Headers */,
Expand Down Expand Up @@ -2877,6 +2884,7 @@
D710D61621949DDC008F54AD /* ARTHttp.h in Headers */,
21113B4729DB484200652C86 /* ARTChannel+Subclass.h in Headers */,
D710D69D21949F00008F54AD /* ARTMsgPackEncoder.h in Headers */,
21BFC2D62D67C58800C05E8B /* ARTRealtimeChannel+Plugins.h in Headers */,
D710D69C21949F00008F54AD /* ARTJsonEncoder.h in Headers */,
D710D5C921949D50008F54AD /* ARTTokenParams+Private.h in Headers */,
D710D52A21949C44008F54AD /* ARTPushChannelSubscription.h in Headers */,
Expand Down Expand Up @@ -4152,7 +4160,10 @@
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_INSTALL_NAME_BASE = "@rpath";
HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Source/Private";
HEADER_SEARCH_PATHS = (
"$(PROJECT_DIR)/Source/Private",
"$(PROJECT_DIR)/AblyPlugin/include",
);
INFOPLIST_FILE = "Source/Info-iOS.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
Expand All @@ -4179,7 +4190,10 @@
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_INSTALL_NAME_BASE = "@rpath";
HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Source/Private";
HEADER_SEARCH_PATHS = (
"$(PROJECT_DIR)/Source/Private",
"$(PROJECT_DIR)/AblyPlugin/include",
);
INFOPLIST_FILE = "Source/Info-iOS.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -4352,7 +4366,10 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_VERSION = A;
GCC_C_LANGUAGE_STANDARD = gnu11;
HEADER_SEARCH_PATHS = "$(SRCROOT)/include";
HEADER_SEARCH_PATHS = (
"$(SRCROOT)/include",
"$(PROJECT_DIR)/AblyPlugin/include",
);
INFOPLIST_FILE = "Source/Info-macOS.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -4392,7 +4409,10 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_VERSION = A;
GCC_C_LANGUAGE_STANDARD = gnu11;
HEADER_SEARCH_PATHS = "$(SRCROOT)/include";
HEADER_SEARCH_PATHS = (
"$(SRCROOT)/include",
"$(PROJECT_DIR)/AblyPlugin/include",
);
INFOPLIST_FILE = "Source/Info-macOS.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -4427,7 +4447,10 @@
DEFINES_MODULE = YES;
DYLIB_INSTALL_NAME_BASE = "@rpath";
GCC_C_LANGUAGE_STANDARD = gnu11;
HEADER_SEARCH_PATHS = "$(SRCROOT)/include";
HEADER_SEARCH_PATHS = (
"$(SRCROOT)/include",
"$(PROJECT_DIR)/AblyPlugin/include",
);
INFOPLIST_FILE = "Source/Info-tvOS.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -4466,7 +4489,10 @@
DEFINES_MODULE = YES;
DYLIB_INSTALL_NAME_BASE = "@rpath";
GCC_C_LANGUAGE_STANDARD = gnu11;
HEADER_SEARCH_PATHS = "$(SRCROOT)/include";
HEADER_SEARCH_PATHS = (
"$(SRCROOT)/include",
"$(PROJECT_DIR)/AblyPlugin/include",
);
INFOPLIST_FILE = "Source/Info-tvOS.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
Expand Down
22 changes: 22 additions & 0 deletions AblyPlugin/APPluginAPI.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#import "APPluginAPI.h"
#import "ARTRealtimeChannel+Plugins.h"

@implementation APPluginAPI

+ (void)setPluginDataValue:(nonnull id)value
forKey:(nonnull NSString *)key
channel:(nonnull ARTRealtimeChannel *)channel {
[channel setPluginDataValue:value forKey:key];
}

+ (nullable id)pluginDataValueForKey:(nonnull NSString *)key
channel:(nonnull ARTRealtimeChannel *)channel {
return [channel pluginDataValueForKey:key];
}

+ (void)addPluginProtocolMessageListener:(APProtocolMessageListener)listener
channel:(nonnull ARTRealtimeChannel *)channel {
[channel addPluginProtocolMessageListener:listener];
}

@end
24 changes: 24 additions & 0 deletions AblyPlugin/include/APLiveObjectsPlugin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#import <Foundation/Foundation.h>

@class ARTRealtimeChannel;
@protocol APLiveObjectsPluginProtocol;

NS_ASSUME_NONNULL_BEGIN

// The `AblyLiveObjects.Plugin` class will _informally_ conform to this (informally so that we don't have to expose this protocol publicly); we keep this protocol simple because there will be no compiler checking
NS_SWIFT_NAME(LiveObjectsPluginFactoryProtocol)
@protocol APLiveObjectsPluginFactoryProtocol <NSObject>

+ (id<APLiveObjectsPluginProtocol>)createPlugin;

@end

// An internal class of `AblyLiveObjects` will conform to this; this protocol can be complex because compiler will check conformance
NS_SWIFT_NAME(LiveObjectsPluginProtocol)
@protocol APLiveObjectsPluginProtocol <NSObject>

- (void)prepareChannel:(ARTRealtimeChannel *)channel;

@end

NS_ASSUME_NONNULL_END
27 changes: 27 additions & 0 deletions AblyPlugin/include/APPluginAPI.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@import Foundation;

@class ARTRealtimeChannel;
@class ARTProtocolMessage;

NS_ASSUME_NONNULL_BEGIN

NS_SWIFT_NAME(ProtocolMessageListener)
typedef void (^APProtocolMessageListener)(ARTProtocolMessage *);

NS_SWIFT_NAME(PluginAPI)
@interface APPluginAPI: NSObject

+ (void)setPluginDataValue:(id)value
forKey:(NSString *)key
channel:(ARTRealtimeChannel *)channel;

+ (nullable id)pluginDataValueForKey:(NSString *)key
channel:(ARTRealtimeChannel *)channel;

// Listener will be called each time a protocol message is received
+ (void)addPluginProtocolMessageListener:(APProtocolMessageListener)listener
channel:(ARTRealtimeChannel *)channel;

@end

NS_ASSUME_NONNULL_END
1 change: 1 addition & 0 deletions AblyPlugin/include/ARTProtocolMessage.h
15 changes: 15 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ let package = Package(
name: "Ably",
targets: ["Ably"]
),
.library(
name: "AblyPlugin",
targets: ["AblyPlugin"]
),
],
dependencies: [
.package(name: "msgpack", url: "/~https://github.com/rvi/msgpack-objective-C", from: "0.4.0"),
Expand Down Expand Up @@ -46,6 +50,17 @@ let package = Package(
.headerSearchPath("SocketRocket/Internal/RunLoop"),
.headerSearchPath("SocketRocket/Internal/Delegate"),
.headerSearchPath("SocketRocket/Internal/IOConsumer"),
.headerSearchPath("../AblyPlugin/include"),
]
),
.target(
name: "AblyPlugin",
dependencies: [
.byName(name: "Ably")
],
path: "AblyPlugin",
cSettings: [
.headerSearchPath("../Source/PrivateHeaders/Ably")
]
)
]
Expand Down
3 changes: 3 additions & 0 deletions Source/ARTClientOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#import "ARTNSString+ARTUtil.h"
#import "ARTTestClientOptions.h"

const ARTPluginName ARTPluginNameLiveObjects = @"LiveObjects";

NSString *ARTDefaultEnvironment = nil;

@interface ARTClientOptions ()
Expand Down Expand Up @@ -140,6 +142,7 @@ - (id)copyWithZone:(NSZone *)zone {
options.transportParams = self.transportParams;
options.agents = self.agents;
options.testOptions = self.testOptions;
options.plugins = self.plugins;

return options;
}
Expand Down
37 changes: 37 additions & 0 deletions Source/ARTRealtimeChannel.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "ARTRealtimeChannel+Private.h"
#import "ARTRealtimeChannel+Plugins.h"
#import "ARTChannel+Private.h"
#import "ARTChannel+Subclass.h"
#import "ARTDataQuery+Private.h"
Expand Down Expand Up @@ -35,6 +36,13 @@
#if TARGET_OS_IPHONE
#import "ARTPushChannel+Private.h"
#endif
#import "APLiveObjectsPlugin.h"

@interface ARTRealtimeChannel ()

@property (nonatomic, readonly) NSMutableDictionary<NSString *, id> *pluginData;

@end

@implementation ARTRealtimeChannel {
ARTQueuedDealloc *_dealloc;
Expand All @@ -57,10 +65,29 @@ - (instancetype)initWithInternal:(ARTRealtimeChannelInternal *)internal queuedDe
if (self) {
_internal = internal;
_dealloc = dealloc;
_pluginData = [[NSMutableDictionary alloc] init];

if (internal.realtime.options.plugins[ARTPluginNameLiveObjects]) {
Class<APLiveObjectsPluginFactoryProtocol> liveObjectsFactoryClass = internal.realtime.options.plugins[ARTPluginNameLiveObjects];
id<APLiveObjectsPluginProtocol> liveObjectsPlugin = [liveObjectsFactoryClass createPlugin];
[liveObjectsPlugin prepareChannel:self];
}
}
return self;
}

- (void)setPluginDataValue:(id)value forKey:(NSString *)key {
[self.pluginData setValue:value forKey:key];
}

- (id)pluginDataValueForKey:(NSString *)key {
return self.pluginData[key];
}

- (void)addPluginProtocolMessageListener:(ARTProtocolMessageListener)listener {
[self.internal addPluginProtocolMessageListener:listener];
}

- (NSString *)name {
return _internal.name;
}
Expand Down Expand Up @@ -243,6 +270,7 @@ @interface ARTRealtimeChannelInternal () {
@interface ARTRealtimeChannelInternal ()

@property (nonatomic, readonly) ARTAttachRetryState *attachRetryState;
@property (nonatomic, readonly) NSMutableArray<ARTProtocolMessageListener> *pluginProtocolMessageListeners;

@end

Expand Down Expand Up @@ -274,10 +302,15 @@ - (instancetype)initWithRealtime:(ARTRealtimeInternal *)realtime andName:(NSStri
_attachRetryState = [[ARTAttachRetryState alloc] initWithRetryDelayCalculator:attachRetryDelayCalculator
logger:logger
logMessagePrefix:[NSString stringWithFormat:@"RT: %p C:%p ", _realtime, self]];
_pluginProtocolMessageListeners = [[NSMutableArray alloc] init];
}
return self;
}

- (void)addPluginProtocolMessageListener:(ARTProtocolMessageListener)listener {
[self.pluginProtocolMessageListeners addObject:listener];
}

- (ARTRealtimeChannelState)state {
__block ARTRealtimeChannelState ret;
dispatch_sync(_queue, ^{
Expand Down Expand Up @@ -628,6 +661,10 @@ - (void)onChannelMessage:(ARTProtocolMessage *)message {
ARTLogWarn(self.logger, @"R:%p C:%p (%@) unknown ARTProtocolMessage action: %tu", _realtime, self, self.name, message.action);
break;
}

for (ARTProtocolMessageListener listener in self.pluginProtocolMessageListeners) {
listener(message);
}
}

- (void)setAttached:(ARTProtocolMessage *)message {
Expand Down
1 change: 1 addition & 0 deletions Source/Ably.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,6 @@ framework module Ably {
header "ARTPendingMessage.h"
header "ARTEncoder.h"
header "ARTDeviceStorage.h"
header "ARTRealtimeChannel+Plugins.h"
}
}
16 changes: 16 additions & 0 deletions Source/PrivateHeaders/Ably/ARTRealtimeChannel+Plugins.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#import <Ably/ARTRealtimeChannel.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^ARTProtocolMessageListener)(ARTProtocolMessage *);

@interface ARTRealtimeChannel ()

- (void)setPluginDataValue:(id)value forKey:(NSString *)key;
- (nullable id)pluginDataValueForKey:(NSString *)key;

- (void)addPluginProtocolMessageListener:(ARTProtocolMessageListener)listener;

@end

NS_ASSUME_NONNULL_END
3 changes: 3 additions & 0 deletions Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import <Ably/ARTRealtime+Private.h>
#import <Ably/ARTQueuedDealloc.h>
#import <Ably/ARTPushChannel+Private.h>
#import <Ably/ARTRealtimeChannel+Plugins.h>

@class ARTProtocolMessage;
@class ARTRealtimePresenceInternal;
Expand Down Expand Up @@ -92,6 +93,8 @@ NS_ASSUME_NONNULL_BEGIN

- (void)setOptions:(ARTRealtimeChannelOptions *_Nullable)options callback:(nullable ARTCallback)callback;

- (void)addPluginProtocolMessageListener:(ARTProtocolMessageListener)listener;

#pragma mark ARTEventEmitter

ART_EMBED_INTERFACE_EVENT_EMITTER(ARTChannelEvent, ARTChannelStateChange *)
Expand Down
Loading

0 comments on commit c624de3

Please sign in to comment.