From 54009fddf2adedd237133ed866359e64a867bacd Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Sat, 27 Jun 2020 16:55:11 +0530 Subject: [PATCH 01/15] Synchronous API for creating instances and Hot-reload fix on Android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Sync + Lazy = Slazy!!! 😉 1. All platform calls from dart side are made lazy 2. Singleton Ably library instances on Android side (iOS will be caught up) ## Hot-reload issue 3. register ably is now used to dispose off any registered platform instances 4. StreamsChannel source is duplicated on android side and necessary changes are made to make hot-reload work well --- README.md | 30 +-- .../plugin/AblyEventStreamHandler.java | 59 ++--- .../flutter/plugin/AblyFlutterMessage.java | 5 +- .../flutter/plugin/AblyFlutterPlugin.java | 14 +- .../io/ably/flutter/plugin/AblyLibrary.java | 31 +-- .../ably/flutter/plugin/AblyMessageCodec.java | 2 +- .../flutter/plugin/AblyMethodCallHandler.java | 74 ++----- .../ably/flutter/plugin/StreamsChannel.java | 205 ++++++++++++++++++ example/lib/main.dart | 73 ++++--- lib/src/ably_implementation.dart | 93 ++++---- lib/src/codec.dart | 4 +- lib/src/impl/message.dart | 8 +- lib/src/impl/platform_object.dart | 49 ++++- lib/src/impl/realtime/channels.dart | 20 +- lib/src/impl/realtime/connection.dart | 9 +- lib/src/impl/realtime/realtime.dart | 26 ++- lib/src/impl/rest/channels.dart | 20 +- lib/src/impl/rest/rest.dart | 20 +- lib/src/interface.dart | 21 +- pubspec.lock | 35 +-- test/ably_flutter_plugin_test.dart | 41 ++-- 21 files changed, 539 insertions(+), 300 deletions(-) create mode 100644 android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java diff --git a/README.md b/README.md index c886700af..a8668a154 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,9 @@ site to discuss privately. import 'package:ably_flutter_plugin/ably.dart' as ably; ``` -##### Create an Ably instance +##### Accessing Ably instance -```dart -final ably.Ably ablyPlugin = ably.Ably(); -``` +Ably library instance can be accessed as `ably.Ably` ##### create a ClientOptions @@ -57,7 +55,7 @@ clientOptions.logLevel = ably.LogLevel.verbose; //optional ##### Rest API ```dart -ably.Rest rest = await ablyPlugin.createRest(options: clientOptions); +ably.Rest rest = ably.Ably.Rest(options: clientOptions); ``` Getting a channel instance @@ -69,20 +67,24 @@ ably.RestChannel channel = rest.channels.get('test'); Publish rest messages ```dart -//passing both name and data -await channel.publish(name: "Hello", data: "Ably"); -//passing just name -await channel.publish(name: "Hello"); -//passing just data -await channel.publish(data: "Ably"); -//publishing an empty message -await channel.publish(); +void publishMessages() async { + //passing both name and data + await channel.publish(name: "Hello", data: "Ably"); + //passing just name + await channel.publish(name: "Hello"); + //passing just data + await channel.publish(data: "Ably"); + //publishing an empty message + await channel.publish(); +} + +publishMessages(); ``` ##### Realtime API ```dart -ably.Realtime realtime = await ablyPlugin.createRealtime(options: clientOptions); +ably.Realtime realtime = ably.Ably.Realtime(options: clientOptions); ``` Listen to connection state change event diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java b/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java index b637e09a7..72537c423 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyEventStreamHandler.java @@ -4,7 +4,6 @@ import android.os.Looper; import io.ably.flutter.plugin.generated.PlatformConstants; -import io.ably.lib.realtime.ChannelStateListener; import io.ably.lib.realtime.ConnectionStateListener; import io.flutter.plugin.common.EventChannel; @@ -16,15 +15,13 @@ * ref: https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.StreamHandler.html * */ public class AblyEventStreamHandler implements EventChannel.StreamHandler { - private final AblyMethodCallHandler methodCallHandler; /** - * Constructor requiring methodCallHandler, as it is a singleton and has all instances stored - * Event listening can be started on an instance picked from the stored instances + * Creating an ablyLibrary instance. + * As ablyLibrary is a singleton, + * all ably object instance will be accessible * */ - AblyEventStreamHandler(AblyMethodCallHandler methodCallHandler){ - this.methodCallHandler = methodCallHandler; - } + private final AblyLibrary ablyLibrary = AblyLibrary.getInstance(); /** * Refer to the comments on AblyMethodCallHandler.MethodResultWrapper @@ -62,26 +59,35 @@ private class Listener{ } private class PluginConnectionStateListener extends Listener implements ConnectionStateListener { - PluginConnectionStateListener(EventChannel.EventSink eventSink){super(eventSink);} + + PluginConnectionStateListener(EventChannel.EventSink eventSink){ + super(eventSink); + } + public void onConnectionStateChanged(ConnectionStateChange stateChange){ eventSink.success(stateChange); } + + } + + // Casting stream creation arguments from `Object` into `AblyMessage` + private AblyFlutterMessage getMessage(Object message){ + return ((AblyFlutterMessage>)message).message; } @Override public void onListen(Object object, EventChannel.EventSink uiThreadEventSink) { MainThreadEventSink eventSink = new MainThreadEventSink(uiThreadEventSink); - methodCallHandler.>ablyDo((AblyFlutterMessage)object, (ablyLibrary, message) -> { - String eventName = message.message; - switch(eventName) { - case PlatformConstants.PlatformMethod.onRealtimeConnectionStateChanged: - connectionStateListener = new PluginConnectionStateListener(eventSink); - ablyLibrary.getRealtime(message.handle).connection.on(connectionStateListener); - return; - default: - eventSink.error("unhandled event", null, null); - } - }); + AblyFlutterMessage message = getMessage(object); + String eventName = message.message; + switch(eventName) { + case PlatformConstants.PlatformMethod.onRealtimeConnectionStateChanged: + connectionStateListener = new PluginConnectionStateListener(eventSink); + ablyLibrary.getRealtime(message.handle).connection.on(connectionStateListener); + return; + default: + eventSink.error("unhandled event", null, null); + } } @Override @@ -90,15 +96,12 @@ public void onCancel(Object object) { System.out.println("Cannot process null input on cancel"); return; } - methodCallHandler.>ablyDo((AblyFlutterMessage)object, (ablyLibrary, message) -> { - String eventName = message.message; - switch (eventName) { - case PlatformConstants.PlatformMethod.onRealtimeConnectionStateChanged: - ablyLibrary.getRealtime(message.handle).connection.off(connectionStateListener); - case PlatformConstants.PlatformMethod.onRealtimeChannelStateChanged: - // ablyLibrary.getRealtime(handle).connection.off(connectionStateListener); - } - }); + AblyFlutterMessage message = getMessage(object); + String eventName = message.message; + switch (eventName) { + case PlatformConstants.PlatformMethod.onRealtimeConnectionStateChanged: + ablyLibrary.getRealtime(message.handle).connection.off(connectionStateListener); + } } } diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyFlutterMessage.java b/android/src/main/java/io/ably/flutter/plugin/AblyFlutterMessage.java index 25758dfeb..1681bae2f 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyFlutterMessage.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyFlutterMessage.java @@ -4,10 +4,7 @@ class AblyFlutterMessage { final Long handle; final T message; - AblyFlutterMessage(final Long handle, final T message) { - if (null == handle) { - throw new NullPointerException("handle cannot be null."); - } + AblyFlutterMessage(final T message, final Long handle) { if (null == message) { throw new NullPointerException("message cannot be null."); } diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyFlutterPlugin.java b/android/src/main/java/io/ably/flutter/plugin/AblyFlutterPlugin.java index ec034982d..5536c25ac 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyFlutterPlugin.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyFlutterPlugin.java @@ -2,7 +2,6 @@ import androidx.annotation.NonNull; -import app.loup.streams_channel.StreamsChannel; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; @@ -35,14 +34,17 @@ public static void registerWith(Registrar registrar) { private static void setupChannels(BinaryMessenger messenger){ MethodCodec codec = createCodec(); - AblyMethodCallHandler methodCallHandler = AblyMethodCallHandler.getInstance(); - - final MethodChannel channel = new MethodChannel(messenger, "io.ably.flutter.plugin", codec); - channel.setMethodCallHandler(methodCallHandler); final StreamsChannel streamsChannel = new StreamsChannel(messenger, "io.ably.flutter.stream", codec); - streamsChannel.setStreamHandlerFactory(arguments -> new AblyEventStreamHandler(methodCallHandler)); + streamsChannel.setStreamHandlerFactory(arguments -> new AblyEventStreamHandler()); + AblyMethodCallHandler methodCallHandler = AblyMethodCallHandler.getInstance( + // Streams channel will be created on `register` method call + // and also on every hot-reload + streamsChannel::reset + ); + final MethodChannel channel = new MethodChannel(messenger, "io.ably.flutter.plugin", codec); + channel.setMethodCallHandler(methodCallHandler); } @Override diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyLibrary.java b/android/src/main/java/io/ably/flutter/plugin/AblyLibrary.java index dfdc23766..d6f4d9c13 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyLibrary.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyLibrary.java @@ -8,53 +8,46 @@ import io.ably.lib.types.ClientOptions; class AblyLibrary { - private boolean _disposed = false; + + private static AblyLibrary _instance; private long _nextHandle = 1; + // privatizing default constructor to enforce usage of getInstance + private AblyLibrary(){} + + static synchronized AblyLibrary getInstance() { + if (null == _instance) { + _instance = new AblyLibrary(); + } + return _instance; + } + // using LongSparseArray as suggested by Studio // and as per this answer https://stackoverflow.com/a/31413003 private final LongSparseArray _restInstances = new LongSparseArray<>(); private final LongSparseArray _realtimeInstances = new LongSparseArray<>(); - private void assertNotDisposed() { - if (_disposed) { - throw new IllegalStateException("Instance disposed."); - } - } - long createRealtime(final ClientOptions clientOptions) throws AblyException { - assertNotDisposed(); - final AblyRealtime realtime = new AblyRealtime(clientOptions); _realtimeInstances.put(_nextHandle, realtime); return _nextHandle++; } long createRest(final ClientOptions clientOptions) throws AblyException { - assertNotDisposed(); - final AblyRest rest = new AblyRest(clientOptions); _restInstances.put(_nextHandle, rest); return _nextHandle++; } AblyRealtime getRealtime(final long handle) { - assertNotDisposed(); - return _realtimeInstances.get(handle); } AblyRest getRest(final long handle){ - assertNotDisposed(); - return _restInstances.get(handle); } void dispose() { - assertNotDisposed(); - - _disposed = true; - for(int i=0; i<_realtimeInstances.size(); i++){ long key = _realtimeInstances.keyAt(i); AblyRealtime r = _realtimeInstances.get(key); diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java index e8d53aa8b..eda0f21b3 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java @@ -137,7 +137,7 @@ private AblyFlutterMessage readAblyFlutterMessage(Map jsonMap) { if(type!=null){ message = codecMap.get((byte)(int)type).decode((Map)message); } - return new AblyFlutterMessage<>(handle, message); + return new AblyFlutterMessage<>(message, handle); } private ClientOptions readClientOptions(Map jsonMap) { diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java index eabeb5424..94d8c8acd 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java @@ -18,23 +18,30 @@ import io.flutter.plugin.common.MethodChannel; import io.ably.flutter.plugin.generated.PlatformConstants; + public class AblyMethodCallHandler implements MethodChannel.MethodCallHandler { private static AblyMethodCallHandler _instance; - static synchronized AblyMethodCallHandler getInstance() { + public interface OnHotRestart { + // this method will be called on every fresh start and on every hot restart + void trigger(); + } + + private OnHotRestart onHotRestartListener; + + static synchronized AblyMethodCallHandler getInstance(OnHotRestart listener) { // TODO decide why singleton instance is required! if (null == _instance) { - _instance = new AblyMethodCallHandler(); + _instance = new AblyMethodCallHandler(listener); } return _instance; } private final Map> _map; - private AblyLibrary _ably; - private Long _ablyHandle; - private long _nextRegistration = 1; + private AblyLibrary _ably = AblyLibrary.getInstance(); - private AblyMethodCallHandler() { + private AblyMethodCallHandler(OnHotRestart listener) { + this.onHotRestartListener = listener; _map = new HashMap<>(); _map.put(PlatformConstants.PlatformMethod.getPlatformVersion, this::getPlatformVersion); _map.put(PlatformConstants.PlatformMethod.getVersion, this::getVersion); @@ -48,7 +55,6 @@ private AblyMethodCallHandler() { _map.put(PlatformConstants.PlatformMethod.createRealtimeWithOptions, this::createRealtimeWithOptions); _map.put(PlatformConstants.PlatformMethod.connectRealtime, this::connectRealtime); _map.put(PlatformConstants.PlatformMethod.closeRealtime, this::closeRealtime); - } // MethodChannel.Result wrapper that responds on the platform thread. @@ -69,36 +75,18 @@ private static class MethodResultWrapper implements MethodChannel.Result { @Override public void success(final Object result) { - handler.post( - new Runnable() { - @Override - public void run() { - methodResult.success(result); - } - }); + handler.post(() -> methodResult.success(result)); } @Override public void error( final String errorCode, final String errorMessage, final Object errorDetails) { - handler.post( - new Runnable() { - @Override - public void run() { - methodResult.error(errorCode, errorMessage, errorDetails); - } - }); + handler.post(() -> methodResult.error(errorCode, errorMessage, errorDetails)); } @Override public void notImplemented() { - handler.post( - new Runnable() { - @Override - public void run() { - methodResult.notImplemented(); - } - }); + handler.post(() -> methodResult.notImplemented()); } } @@ -117,26 +105,10 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result } private void register(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - final Long handle = _nextRegistration++; - - if (null != _ablyHandle) { - System.out.println("Disposing of previous Ably instance (# " + _ablyHandle + ")."); - } - - // Setting _ablyHandle to null when _ably is not null indicates that we're in the process - // of asynchronously disposing of the old instance. - _ablyHandle = null; - - // TODO actually do this next bit asynchronously like we do for iOS - if (null != _ably) { - _ably.dispose(); - } - - System.out.println("Creating new Ably instance (# " + handle + ")."); - _ably = new AblyLibrary(); - _ablyHandle = handle; - - result.success(handle); + System.out.println("Registering library instance to clean up any existing instnaces"); + onHotRestartListener.trigger(); + _ably.dispose(); + result.success(null); } private void createRestWithOptions(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { @@ -202,11 +174,7 @@ private void closeRealtime(@NonNull MethodCall call, @NonNull MethodChannel.Resu }); } - public void ablyDo(final AblyFlutterMessage message, final BiConsumer consumer) { - if (!message.handle.equals(_ablyHandle)) { - // TODO an error response, perhaps? or allow Dart side to understand null response? - return; - } + void ablyDo(final AblyFlutterMessage message, final BiConsumer consumer) { consumer.accept(_ably, (Arguments)message.message); } diff --git a/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java b/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java new file mode 100644 index 000000000..04109233a --- /dev/null +++ b/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java @@ -0,0 +1,205 @@ +// This file is derivative of work derived from original work at https://github.com/loup-v/streams_channel +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 +// Original file @ https://github.com/loup-v/streams_channel/blob/master/android/src/main/java/app/loup/streams_channel/StreamsChannel.java + + +package io.ably.flutter.plugin; + +import android.annotation.SuppressLint; +import android.util.Log; + +import androidx.annotation.UiThread; + +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import app.loup.streams_channel.BuildConfig; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; +import io.flutter.plugin.common.BinaryMessenger.BinaryReply; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodCodec; + + +final class StreamsChannel { + + public interface StreamHandlerFactory { + EventChannel.StreamHandler create(Object arguments); + } + + private static final String TAG = "StreamsChannel#"; + + private final BinaryMessenger messenger; + private final String name; + private final MethodCodec codec; + private IncomingStreamRequestHandler incomingStreamRequestHandler; + + StreamsChannel(BinaryMessenger messenger, String name, MethodCodec codec) { + if (BuildConfig.DEBUG) { + if (messenger == null) { + Log.e(TAG, "Parameter messenger must not be null."); + } + if (name == null) { + Log.e(TAG, "Parameter name must not be null."); + } + if (codec == null) { + Log.e(TAG, "Parameter codec must not be null."); + } + } + this.messenger = messenger; + this.name = name; + this.codec = codec; + } + + @UiThread + void setStreamHandlerFactory(final StreamHandlerFactory factory) { + incomingStreamRequestHandler = new IncomingStreamRequestHandler(factory); + messenger.setMessageHandler(name, incomingStreamRequestHandler); + } + + void reset(){ + incomingStreamRequestHandler.clearAll(); + } + + private final class IncomingStreamRequestHandler implements BinaryMessageHandler { + private final StreamHandlerFactory factory; + private final ConcurrentHashMap streams = new ConcurrentHashMap<>(); + private final ConcurrentHashMap listenerArguments = new ConcurrentHashMap<>(); + + IncomingStreamRequestHandler(StreamHandlerFactory factory) { + this.factory = factory; + } + + @Override + public void onMessage(ByteBuffer message, final BinaryReply reply) { + final MethodCall call = codec.decodeMethodCall(message); + final String[] methodParts = call.method.split("#"); + + if (methodParts.length != 2) { + reply.reply(null); + return; + } + + final int id; + try { + id = Integer.parseInt(methodParts[1]); + } catch (NumberFormatException e) { + reply.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); + return; + } + + final String method = methodParts[0]; + switch (method) { + case "listen": + onListen(id, call.arguments, reply); + break; + case "cancel": + onCancel(id, call.arguments, reply); + break; + default: + reply.reply(null); + break; + } + } + + private void onListen(int id, Object arguments, BinaryReply callback) { + final Stream stream = new Stream(new IncomingStreamRequestHandler.EventSinkImplementation(id), factory.create(arguments)); + streams.putIfAbsent(id, stream); + listenerArguments.put(id, arguments); + + try { + stream.handler.onListen(arguments, stream.sink); + callback.reply(codec.encodeSuccessEnvelope(null)); + } catch (RuntimeException e) { + streams.remove(id); + logError(id, "Failed to open event stream", e); + callback.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); + } + } + + private void onCancel(int id, Object arguments, BinaryReply callback) { + final Stream oldStream = streams.remove(id); + + if (oldStream != null) { + try { + oldStream.handler.onCancel(arguments); + if(callback!=null) callback.reply(codec.encodeSuccessEnvelope(null)); + } catch (RuntimeException e) { + logError(id, "Failed to close event stream", e); + if(callback!=null) callback.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); + } + } else { + if(callback!=null) callback.reply(codec.encodeErrorEnvelope("error", "No active stream to cancel", null)); + } + } + + void clearAll(){ + for (ConcurrentHashMap.Entry entry : incomingStreamRequestHandler.streams.entrySet()){ + int id = entry.getKey(); + Object arguments = listenerArguments.get(id); + this.onCancel(id, arguments, null); + } + } + + private void logError(int id, String message, Throwable e) { + Log.e(TAG + name, String.format("%s [id=%d]", message, id), e); + } + + private final class EventSinkImplementation implements EventChannel.EventSink { + + final int id; + final String name; + final AtomicBoolean hasEnded = new AtomicBoolean(false); + + @SuppressLint("DefaultLocale") + private EventSinkImplementation(int id) { + this.id = id; + this.name = String.format("%s#%d", StreamsChannel.this.name, id); + } + + @Override + @UiThread + public void success(Object event) { + if (hasEnded.get() || streams.get(id).sink != this) { + return; + } + StreamsChannel.this.messenger.send(name, codec.encodeSuccessEnvelope(event)); + } + + @Override + @UiThread + public void error(String errorCode, String errorMessage, Object errorDetails) { + if (hasEnded.get() || streams.get(id).sink != this) { + return; + } + StreamsChannel.this.messenger.send( + name, + codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails)); + } + + @Override + @UiThread + public void endOfStream() { + if (hasEnded.getAndSet(true) || streams.get(id).sink != this) { + return; + } + StreamsChannel.this.messenger.send(name, null); + } + } + + } + + private static class Stream { + final EventChannel.EventSink sink; + final EventChannel.StreamHandler handler; + + private Stream(EventChannel.EventSink sink, EventChannel.StreamHandler handler) { + this.sink = sink; + this.handler = handler; + } + } + +} diff --git a/example/lib/main.dart b/example/lib/main.dart index c471f4e57..02a674faf 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -26,7 +26,6 @@ class _MyAppState extends State { provisioning.AppKey _appKey; OpState _realtimeCreationState = OpState.NotStarted; OpState _restCreationState = OpState.NotStarted; - ably.Ably _ablyPlugin; ably.Realtime _realtime; ably.Rest _rest; ably.ConnectionState _realtimeConnectionState; @@ -41,19 +40,17 @@ class _MyAppState extends State { Future initPlatformState() async { print('initPlatformState()'); - final ably.Ably ablyPlugin = ably.Ably(); - String platformVersion; String ablyVersion; // Platform messages may fail, so we use a try/catch PlatformException. try { - platformVersion = await ablyPlugin.platformVersion; + platformVersion = await ably.Ably.platformVersion; } on PlatformException { platformVersion = 'Failed to get platform version.'; } try { - ablyVersion = await ablyPlugin.version; + ablyVersion = await ably.Ably.version; } on PlatformException { ablyVersion = 'Failed to get Ably version.'; } @@ -68,7 +65,6 @@ class _MyAppState extends State { print('set state'); _platformVersion = platformVersion; _ablyVersion = ablyVersion; - _ablyPlugin = ablyPlugin; }); } @@ -102,7 +98,7 @@ class _MyAppState extends State { ably.Rest rest; try{ - rest = await _ablyPlugin.createRest(options: clientOptions); + rest = ably.Ably.Rest(options: clientOptions); } catch (error) { print('Error creating Ably Rest: ${error}'); setState(() { _restCreationState = OpState.Failed; }); @@ -141,18 +137,19 @@ class _MyAppState extends State { ably.Realtime realtime; try { - realtime = await _ablyPlugin.createRealtime(options: clientOptions); + realtime = ably.Ably.Realtime(options: clientOptions); //One can listen from multiple listeners on the same event, // and must cancel each subscription one by one //RETAINING LISTENER - α + print("starting α listener"); realtime.connection.on().listen((ably.ConnectionStateChange stateChange) async { print('RETAINING LISTENER α :: Change event arrived!: ${stateChange.event}'); setState(() { _realtimeConnectionState = stateChange.current; }); }); //DISPOSE ON CONNECTED - Stream stream = realtime.connection.on(); + Stream stream = await realtime.connection.on(); StreamSubscription subscription; subscription = stream.listen((ably.ConnectionStateChange stateChange) async { print('DISPOSABLE LISTENER ω :: Change event arrived!: ${stateChange.event}'); @@ -164,18 +161,22 @@ class _MyAppState extends State { //RETAINING LISTENER - β realtime.connection.on().listen((ably.ConnectionStateChange stateChange) async { print('RETAINING LISTENER β :: Change event arrived!: ${stateChange.event}'); - //NESTED LISTENER - ξ - realtime.connection.on().listen((ably.ConnectionStateChange stateChangeX) async { - //k ξ listeners will be registered and each listener will be called `n-k` times respectively if listener β is called `n` times - print('NESTED LISTENER ξ: ${stateChangeX.event}'); + // NESTED LISTENER - ξ + // will be registered only when connected event is received by β listener + realtime.connection.on().listen(( + ably.ConnectionStateChange stateChange) async { + // k ξ listeners will be registered + // and each listener will be called `n-k` times respectively + // if listener β is called `n` times + print('NESTED LISTENER ξ: ${stateChange.event}'); }); }); StreamSubscription preZetaSubscription; StreamSubscription postZetaSubscription; - preZetaSubscription = realtime.connection.on().listen((ably.ConnectionStateChange stateChangeX) async { + preZetaSubscription = realtime.connection.on().listen((ably.ConnectionStateChange stateChange) async { //This listener "pre ζ" will be cancelled from γ - print('NESTED LISTENER "pre ζ": ${stateChangeX.event}'); + print('NESTED LISTENER "pre ζ": ${stateChange.event}'); }); @@ -188,9 +189,9 @@ class _MyAppState extends State { } }); - postZetaSubscription = realtime.connection.on().listen((ably.ConnectionStateChange stateChangeX) async { + postZetaSubscription = realtime.connection.on().listen((ably.ConnectionStateChange stateChange) async { //This listener "post ζ" will be cancelled from γ - print('NESTED LISTENER "post ζ": ${stateChangeX.event}'); + print('NESTED LISTENER "post ζ": ${stateChange.event}'); }); setState(() { @@ -256,8 +257,8 @@ class _MyAppState extends State { onPressed: () async { print('Sendimg rest message...'); await _rest.channels.get('test').publish( - name: 'Hello', - data: 'Flutter ${++msgCounter}' + name: 'Hello', + data: 'Flutter ${++msgCounter}' ); print('Rest message sent.'); setState(() {}); @@ -277,23 +278,23 @@ class _MyAppState extends State { child: Padding( padding: EdgeInsets.symmetric(horizontal: 36.0), child: Column( - children: [ - Text('Running on: $_platformVersion\n'), - Text('Ably version: $_ablyVersion\n'), - provisionButton(), - Text('App Key: ' + ((_appKey == null) ? 'Ably not provisioned yet.' : _appKey.toString())), - Divider(), - createRealtimeButton(), - Text('Realtime: ' + ((_realtime == null) ? 'Ably Realtime not created yet.' : _realtime.toString())), - Text('Connection Status: $_realtimeConnectionState'), - createRTCConnectButton(), - createRTCloseButton(), - Divider(), - createRestButton(), - Text('Rest: ' + ((_rest == null) ? 'Ably Rest not created yet.' : _rest.toString())), - sendRestMessage(), - Text('Rest: press this button to publish a new message with data "Flutter ${msgCounter+1}"'), - ] + children: [ + Text('Running on: $_platformVersion\n'), + Text('Ably version: $_ablyVersion\n'), + provisionButton(), + Text('App Key: ' + ((_appKey == null) ? 'Ably not provisioned yet.' : _appKey.toString())), + Divider(), + createRealtimeButton(), + Text('Realtime: ' + ((_realtime == null) ? 'Ably Realtime not created yet.' : _realtime.toString())), + Text('Connection Status: $_realtimeConnectionState'), + createRTCConnectButton(), + createRTCloseButton(), + Divider(), + createRestButton(), + Text('Rest: ' + ((_rest == null) ? 'Ably Rest not created yet.' : _rest.toString())), + sendRestMessage(), + Text('Rest: press this button to publish a new message with data "Flutter ${msgCounter+1}"'), + ] ), ), ), diff --git a/lib/src/ably_implementation.dart b/lib/src/ably_implementation.dart index 6a4872e8b..996d5f042 100644 --- a/lib/src/ably_implementation.dart +++ b/lib/src/ably_implementation.dart @@ -1,19 +1,20 @@ import 'dart:async'; -import 'package:ably_flutter_plugin/src/impl/message.dart'; import 'package:ably_flutter_plugin/src/interface.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:ably_flutter_plugin/src/generated/platformconstants.dart' show PlatformMethod; import 'package:streams_channel/streams_channel.dart'; -import '../ably.dart'; +import 'spec/spec.dart' as spec; import 'codec.dart'; import 'impl/platform_object.dart'; import 'impl/realtime/realtime.dart'; import 'impl/rest/rest.dart'; + /// Ably plugin implementation /// Single point of interaction that exposes all necessary APIs to ably objects -class AblyImplementation implements Ably { +class AblyImplementation implements AblyLibrary { /// instance of method channel to interact with android/ios code final MethodChannel methodChannel; @@ -24,79 +25,75 @@ class AblyImplementation implements Ably { /// Storing all platform objects, for easy references/cleanup final List _platformObjects = []; - /// ably handle, created from platform's end - /// This makes sure we are accessing the right Ably instance on platform - /// as users can create multiple [Ably] instances - int _handle; - factory AblyImplementation() { /// Uses our custom message codec so that we can pass Ably types to the /// platform implementations. StandardMethodCodec codec = StandardMethodCodec(Codec()); return AblyImplementation._constructor( - MethodChannel('io.ably.flutter.plugin', codec), - StreamsChannel('io.ably.flutter.stream', codec) + MethodChannel('io.ably.flutter.plugin', codec), + StreamsChannel('io.ably.flutter.stream', codec) ); } + bool _initialized = false; + AblyImplementation._constructor(this.methodChannel, this.streamsChannel); - /// Registering instance with ably. - /// On registration, older ably instance id destroyed! - /// TODO check if this is desired behavior in case if 2 different ably instances are created in app - Future _register() async => (null != _handle) ? _handle : _handle = await methodChannel.invokeMethod(PlatformMethod.registerAbly); + void _init(){ + methodChannel.invokeMethod(PlatformMethod.registerAbly).then((_){ + _initialized = true; + }); + } + + /// Initializing ably on platform side by invoking `register` platform method. + /// Register will clear any stale instances on platform. + Future _initialize() async { + if(_initialized) return; + _init(); + // if `_initialized` is false => initialization is in progress. + // Let's wait for 250ms and retry + // + // this is required as many asynchronous `_initialize` calls + // will be invoked from different Ably instances + while(true){ + await Future.delayed(Duration(milliseconds: 250)); + if(_initialized) return; + } + } + + Future invoke(String method, [dynamic arguments]) async { + await _initialize(); + return await methodChannel.invokeMethod(method, arguments); + } @override - Future createRealtime({ - ClientOptions options, + spec.Realtime Realtime({ + spec.ClientOptions options, final String key - }) async { + }) { // TODO options.authCallback // TODO options.logHandler - final handle = await _register(); - final message = AblyMessage(handle, options); - final r = RealtimePlatformObject( - handle, - this, - await methodChannel.invokeMethod( - PlatformMethod.createRealtimeWithOptions, - message - ), - options: options, - key: key - ); + final r = RealtimePlatformObject(options: options, key: key); _platformObjects.add(r); return r; } @override - Future createRest({ - ClientOptions options, + spec.Rest Rest({ + spec.ClientOptions options, final String key - }) async { + }) { // TODO options.authCallback // TODO options.logHandler - final handle = await _register(); - final message = AblyMessage(handle, options); - final r = RestPlatformObject( - handle, - this, - await methodChannel.invokeMethod( - PlatformMethod.createRestWithOptions, - message - ), - options: options, - key: key - ); + final r = RestPlatformObject(options: options, key: key); _platformObjects.add(r); return r; } @override - Future get platformVersion async => - await methodChannel.invokeMethod(PlatformMethod.getPlatformVersion); + Future get platformVersion async => await invoke(PlatformMethod.getPlatformVersion); @override - Future get version async => - await methodChannel.invokeMethod(PlatformMethod.getVersion); + Future get version async => await invoke(PlatformMethod.getVersion); + } diff --git a/lib/src/codec.dart b/lib/src/codec.dart index 297d5eda6..2c46f337d 100644 --- a/lib/src/codec.dart +++ b/lib/src/codec.dart @@ -198,7 +198,7 @@ class Codec extends StandardMessageCodec { int codecType = getCodecType(v.message); dynamic message = (v.message==null)?null:(codecType == null)?v.message:codecMap[codecType].encode(v.message); Map jsonMap = {}; - writeToJson(jsonMap, TxAblyMessage.registrationHandle, v.registrationHandle); + writeToJson(jsonMap, TxAblyMessage.registrationHandle, v.handle); writeToJson(jsonMap, TxAblyMessage.type, codecType); writeToJson(jsonMap, TxAblyMessage.message, message); return jsonMap; @@ -294,8 +294,8 @@ class Codec extends StandardMessageCodec { message = codecMap[type].decode(toJsonMap(jsonMap[TxAblyMessage.message])); } return AblyMessage( - jsonMap[TxAblyMessage.registrationHandle] as int, message, + handle: jsonMap[TxAblyMessage.registrationHandle] as int, type: type ); } diff --git a/lib/src/impl/message.dart b/lib/src/impl/message.dart index ed1b40168..51bca476e 100644 --- a/lib/src/impl/message.dart +++ b/lib/src/impl/message.dart @@ -1,7 +1,11 @@ class AblyMessage { - final int registrationHandle; + + final int handle; final int type; final dynamic message; - AblyMessage(this.registrationHandle, this.message, {this.type}); + AblyMessage(dynamic message, {this.handle, this.type}): + assert(message!=null), + this.message = message; + } diff --git a/lib/src/impl/platform_object.dart b/lib/src/impl/platform_object.dart index 3b371980d..861478758 100644 --- a/lib/src/impl/platform_object.dart +++ b/lib/src/impl/platform_object.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:ably_flutter_plugin/src/interface.dart'; import 'package:ably_flutter_plugin/src/ably_implementation.dart'; import 'package:ably_flutter_plugin/src/impl/message.dart'; import 'package:flutter/services.dart'; @@ -8,18 +11,34 @@ import 'package:streams_channel/streams_channel.dart'; /// where that live counterpart is held as a strong reference by the plugin /// implementation. abstract class PlatformObject { - final int _ablyHandle; - final AblyImplementation _ablyPlugin; - final int _handle; + int _handle; - PlatformObject(this._ablyHandle, this._ablyPlugin, this._handle); + PlatformObject(){ + this.handle; //fetching asynchronous handle proactively... + } @override String toString() => 'Ably Platform Object $_handle'; - get handle => _handle; - get ablyHandle => _ablyHandle; + Future createPlatformInstance(); + + bool _registering = false; + Future get handle async { + if(_registering){ + //if handle is queried while already being fetched from remote, it must be put on hold.. + await Future.delayed(Duration(milliseconds: 250)); + return await this.handle; + } + if(_handle == null) { + _registering = true; + _handle = await createPlatformInstance(); + _registering = false; + } + return _handle; + } + + AblyImplementation get _ablyPlugin => (Ably as AblyImplementation); MethodChannel get methodChannel => _ablyPlugin.methodChannel; StreamsChannel get eventChannel => _ablyPlugin.streamsChannel; @@ -30,14 +49,24 @@ abstract class PlatformObject { /// Call a method. Future invoke(final String method, [final dynamic argument]) async { + int _handle = await handle; final message = (null != argument) - ? AblyMessage(_ablyHandle, AblyMessage(_handle, argument)) - : AblyMessage(_ablyHandle, _handle); - return await methodChannel.invokeMethod(method, message); + ? AblyMessage(AblyMessage(argument, handle: _handle)) + : AblyMessage(_handle); + return await Ably.invoke(method, message); + } + + Future> _listen(final String method) async { + return eventChannel.receiveBroadcastStream( + AblyMessage(AblyMessage(method, handle: await handle)) + ); } + /// Listen for events Stream listen(final String method){ - return eventChannel.receiveBroadcastStream(AblyMessage(_ablyHandle, AblyMessage(_handle, method))); + StreamController controller = StreamController(); + _listen(method).then(controller.addStream); + return controller.stream; } } diff --git a/lib/src/impl/realtime/channels.dart b/lib/src/impl/realtime/channels.dart index fdab04c01..63331f9d4 100644 --- a/lib/src/impl/realtime/channels.dart +++ b/lib/src/impl/realtime/channels.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:ably_flutter_plugin/ably.dart'; +import 'package:ably_flutter_plugin/src/impl/realtime/realtime.dart'; import 'package:ably_flutter_plugin/src/spec/push/channels.dart'; import 'package:ably_flutter_plugin/src/spec/spec.dart' as spec; import 'package:flutter/services.dart'; @@ -21,8 +22,15 @@ class RealtimePlatformChannel extends PlatformObject implements spec.RealtimeCha @override spec.RealtimePresence presence; - RealtimePlatformChannel(int ablyHandle, Ably ablyPlugin, int restHandle, this.ably, this.name, this.options) - : super(ablyHandle, ablyPlugin, restHandle); + RealtimePlatformChannel(this.ably, this.name, this.options): super(); + + RealtimePlatformObject get realtimePlatformObject => this.ably as RealtimePlatformObject; + + /// createPlatformInstance will return realtimePlatformObject's handle + /// as that is what will be required in platforms end to find realtime instance + /// and send message to channel + @override + Future createPlatformInstance() async => await realtimePlatformObject.handle; @override Future> history([spec.RealtimeHistoryParams params]) { @@ -113,15 +121,11 @@ class RealtimePlatformChannel extends PlatformObject implements spec.RealtimeCha class RealtimePlatformChannels extends spec.RealtimeChannels{ - int ablyHandle; - Ably ablyPlugin; - int restHandle; - - RealtimePlatformChannels(this.ablyHandle, this.ablyPlugin, this.restHandle, spec.AblyBase ably): super(ably); + RealtimePlatformChannels(RealtimePlatformObject ably): super(ably); @override RealtimePlatformChannel createChannel(name, options){ - return RealtimePlatformChannel(ablyHandle, ablyPlugin, restHandle, ably, name, options); + return RealtimePlatformChannel(this.ably, name, options); } } diff --git a/lib/src/impl/realtime/connection.dart b/lib/src/impl/realtime/connection.dart index 5381692eb..826c528ab 100644 --- a/lib/src/impl/realtime/connection.dart +++ b/lib/src/impl/realtime/connection.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:ably_flutter_plugin/ably.dart'; +import 'package:ably_flutter_plugin/src/impl/realtime/realtime.dart'; import '../platform_object.dart'; import '../../spec/spec.dart' show Connection, ConnectionState, ErrorInfo; @@ -8,8 +9,12 @@ import '../../spec/spec.dart' show Connection, ConnectionState, ErrorInfo; class ConnectionPlatformObject extends PlatformObject implements Connection { - ConnectionPlatformObject(int ablyHandle, Ably ablyPlugin, int realtimeHandle) - : super(ablyHandle, ablyPlugin, realtimeHandle); + RealtimePlatformObject realtimePlatformObject; + + ConnectionPlatformObject(this.realtimePlatformObject); + + @override + Future createPlatformInstance() async => await realtimePlatformObject.handle; @override ErrorInfo errorReason; diff --git a/lib/src/impl/realtime/realtime.dart b/lib/src/impl/realtime/realtime.dart index 10495691b..e242d7f4d 100644 --- a/lib/src/impl/realtime/realtime.dart +++ b/lib/src/impl/realtime/realtime.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:ably_flutter_plugin/src/impl/message.dart'; + import '../../../ably.dart'; import '../../spec/spec.dart' as spec; import '../platform_object.dart'; @@ -8,18 +10,28 @@ import 'connection.dart'; class RealtimePlatformObject extends PlatformObject implements spec.Realtime { - RealtimePlatformObject(int ablyHandle, Ably ablyPlugin, int handle, { + RealtimePlatformObject({ ClientOptions options, final String key - }) - :assert(options!=null || key!=null), - connection = ConnectionPlatformObject(ablyHandle, ablyPlugin, handle), - super(ablyHandle, ablyPlugin, handle) { - this.options = (options==null)?ClientOptions.fromKey(key):options; + }) : + assert(options!=null || key!=null), + this.options = (options==null)?ClientOptions.fromKey(key):options, + super() + { + connection = ConnectionPlatformObject(this); } @override - final Connection connection; + Future createPlatformInstance() async => await Ably.invoke( + PlatformMethod.createRealtimeWithOptions, + AblyMessage(options) + ); + + // The _connection instance keeps a reference to this platform object. + // Ideally connection would be final, but that would need 'late final' which is coming. + // https://stackoverflow.com/questions/59449666/initialize-a-final-variable-with-this-in-dart#comment105082936_59450231 + @override + Connection connection; @override Auth auth; diff --git a/lib/src/impl/rest/channels.dart b/lib/src/impl/rest/channels.dart index fcecb231b..1426368e5 100644 --- a/lib/src/impl/rest/channels.dart +++ b/lib/src/impl/rest/channels.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:ably_flutter_plugin/ably.dart'; +import 'package:ably_flutter_plugin/src/impl/rest/rest.dart'; import 'package:ably_flutter_plugin/src/spec/spec.dart' as spec; import 'package:flutter/services.dart'; import '../platform_object.dart'; @@ -20,8 +21,15 @@ class RestPlatformChannel extends PlatformObject implements spec.RestChannel{ @override spec.Presence presence; - RestPlatformChannel(int ablyHandle, Ably ablyPlugin, int restHandle, this.ably, this.name, this.options) - : super(ablyHandle, ablyPlugin, restHandle); + RestPlatformChannel(this.ably, this.name, this.options); + + RestPlatformObject get restPlatformObject => this.ably as RestPlatformObject; + + /// createPlatformInstance will return restPlatformObject's handle + /// as that is what will be required in platforms end to find rest instance + /// and send message to channel + @override + Future createPlatformInstance() async => await restPlatformObject.handle; @override Future> history([spec.RestHistoryParams params]) { @@ -46,15 +54,11 @@ class RestPlatformChannel extends PlatformObject implements spec.RestChannel{ class RestPlatformChannels extends spec.RestChannels{ - int ablyHandle; - int restHandle; - Ably ablyPlugin; - - RestPlatformChannels(this.ablyHandle, this.ablyPlugin, this.restHandle, spec.AblyBase ably): super(ably); + RestPlatformChannels(RestPlatformObject ably): super(ably); @override RestPlatformChannel createChannel(name, options){ - return RestPlatformChannel(ablyHandle, ablyPlugin, restHandle, ably, name, options); + return RestPlatformChannel(this.ably, name, options); } } diff --git a/lib/src/impl/rest/rest.dart b/lib/src/impl/rest/rest.dart index e33da84db..836f02ff8 100644 --- a/lib/src/impl/rest/rest.dart +++ b/lib/src/impl/rest/rest.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:ably_flutter_plugin/src/impl/message.dart'; + import '../../../ably.dart'; import '../../spec/spec.dart' as spec; import '../platform_object.dart'; @@ -8,16 +10,22 @@ import 'channels.dart'; class RestPlatformObject extends PlatformObject implements spec.Rest { - RestPlatformObject(int ablyHandle, Ably ablyPlugin, int handle, { + RestPlatformObject({ ClientOptions options, final String key - }) - :assert(options!=null || key!=null), - super(ablyHandle, ablyPlugin, handle){ - this.options = (options==null)?ClientOptions.fromKey(key):options; - this.channels = RestPlatformChannels(ablyHandle, ablyPlugin, handle, this); + }) : + assert(options!=null || key!=null), + this.options = (options==null)?ClientOptions.fromKey(key):options, + super() + { + this.channels = RestPlatformChannels(this); } + Future createPlatformInstance() async => await Ably.invoke( + PlatformMethod.createRestWithOptions, + AblyMessage(options) + ); + @override Auth auth; diff --git a/lib/src/interface.dart b/lib/src/interface.dart index d9a48a902..83a6a70e1 100644 --- a/lib/src/interface.dart +++ b/lib/src/interface.dart @@ -1,8 +1,13 @@ import 'ably_implementation.dart'; -import 'spec/spec.dart' show Realtime, ClientOptions, Rest; +import 'spec/spec.dart' as spec; -abstract class Ably { - factory Ably() => AblyImplementation(); +abstract class AblyLibrary { + + static final AblyLibrary _instance = AblyImplementation(); + factory AblyLibrary() => _instance; + + /// Invokes a platform method + Future invoke(String method, [dynamic arguments]); /// Returns platform version Future get platformVersion; @@ -12,16 +17,18 @@ abstract class Ably { /// Creates a [Realtime] instance either with [options] or with [key] /// obtained from Ably dashboard - Future createRealtime({ - ClientOptions options, + spec.Realtime Realtime({ + spec.ClientOptions options, final String key }); /// Creates a [Rest] instance either with [options] or with [key] /// obtained from Ably dashboard - Future createRest({ - ClientOptions options, + spec.Rest Rest({ + spec.ClientOptions options, final String key }); } + +final AblyLibrary Ably = AblyLibrary(); diff --git a/pubspec.lock b/pubspec.lock index 7e99931aa..097821d8e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,42 +21,42 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.13" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.5.2" + version: "1.6.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "2.0.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" + version: "1.14.12" convert: dependency: transitive description: @@ -77,7 +77,7 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.4" csslib: dependency: transitive description: @@ -136,7 +136,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.12" io: dependency: transitive description: @@ -262,7 +262,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.3" shelf: dependency: transitive description: @@ -316,7 +316,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.7.0" stack_trace: dependency: transitive description: @@ -358,21 +358,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.9.4" + version: "1.13.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.11" + version: "0.2.15" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.3.1" typed_data: dependency: transitive description: @@ -408,13 +408,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.4" xml: dependency: transitive description: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "3.6.1" yaml: dependency: transitive description: diff --git a/test/ably_flutter_plugin_test.dart b/test/ably_flutter_plugin_test.dart index 70f1c6802..e5f4a1375 100644 --- a/test/ably_flutter_plugin_test.dart +++ b/test/ably_flutter_plugin_test.dart @@ -1,12 +1,8 @@ import 'package:ably_flutter_plugin/ably.dart'; import 'package:ably_flutter_plugin/src/ably_implementation.dart'; -import 'package:ably_flutter_plugin/src/impl/platform_object.dart'; import 'package:ably_flutter_plugin/src/impl/rest/rest.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -// import 'package:ably_flutter_plugin/ably.dart' as ably; - -// TODO make these tests make sense again or get rid of them ///Extension to extract string name from PlatformMethod @@ -19,11 +15,9 @@ extension on PlatformMethod { void main() { - AblyImplementation ably = Ably() as AblyImplementation; - MethodChannel channel = ably.methodChannel; + MethodChannel channel = (Ably as AblyImplementation).methodChannel; TestWidgetsFlutterBinding.ensureInitialized(); - int ablyCounter = 0; int counter = 0; //test constants @@ -33,14 +27,14 @@ void main() { setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { switch(methodCall.method){ + case PlatformMethod.registerAbly: + return true; + case PlatformMethod.getPlatformVersion: return platformVersion; case PlatformMethod.getVersion: return nativeLibraryVersion; - case PlatformMethod.registerAbly: - return ++ablyCounter; - case "createrestWithKey": case PlatformMethod.createRestWithOptions: case PlatformMethod.createRealtimeWithOptions: @@ -50,9 +44,9 @@ void main() { case PlatformMethod.connectRealtime: default: return null; - // eventsOff, - // eventsOn, - // eventOnce, + // eventsOff, + // eventsOn, + // eventOnce, } }); }); @@ -62,41 +56,38 @@ void main() { }); test(PlatformMethod.getPlatformVersion, () async { - expect(await ably.platformVersion, platformVersion); + expect(await Ably.platformVersion, platformVersion); }); test(PlatformMethod.getVersion, () async { - expect(await ably.version, nativeLibraryVersion); + expect(await Ably.version, nativeLibraryVersion); }); test(PlatformMethod.createRestWithOptions, () async { ClientOptions o = ClientOptions(); String host = "http://rest.ably.io/"; o.restHost = host; - RestPlatformObject rest = await ably.createRest(options: o); - expect(rest.ablyHandle, ablyCounter); - expect(rest.handle, counter); + RestPlatformObject rest = Ably.Rest(options: o); + expect(await rest.handle, counter); expect(rest.options.restHost, host); }); test("createRestWithToken", () async { String key = 'TEST-KEY'; - RestPlatformObject rest = await ably.createRest(key: key); - expect(rest.ablyHandle, ablyCounter); - expect(rest.handle, counter); + RestPlatformObject rest = Ably.Rest(key: key); + expect(await rest.handle, counter); expect(rest.options.tokenDetails.token, key); }); test("createRestWithKey", () async { String key = 'TEST:KEY'; - RestPlatformObject rest = await ably.createRest(key: key); - expect(rest.ablyHandle, ablyCounter); - expect(rest.handle, counter); + RestPlatformObject rest = Ably.Rest(key: key); + expect(await rest.handle, counter); expect(rest.options.key, key); }); test("publishMessage", () async { - RestPlatformObject rest = await ably.createRest(key: 'TEST-KEY'); + RestPlatformObject rest = Ably.Rest(key: 'TEST-KEY'); await rest.channels.get('test').publish(name: 'name', data: 'data'); expect(1, 1); }); From 078cca0aa78943b6d9f15c84dde5c0ce6714e4e5 Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Tue, 7 Jul 2020 00:54:25 +0530 Subject: [PATCH 02/15] removing unwanted comments --- test/ably_flutter_plugin_test.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/ably_flutter_plugin_test.dart b/test/ably_flutter_plugin_test.dart index e5f4a1375..f79e96789 100644 --- a/test/ably_flutter_plugin_test.dart +++ b/test/ably_flutter_plugin_test.dart @@ -44,9 +44,6 @@ void main() { case PlatformMethod.connectRealtime: default: return null; - // eventsOff, - // eventsOn, - // eventOnce, } }); }); From 2047fab74b2ea8639519772e09864e9d25f83956 Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Tue, 7 Jul 2020 01:04:30 +0530 Subject: [PATCH 03/15] improve commentary about acquiring the lazy sync handle --- lib/src/impl/platform_object.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/src/impl/platform_object.dart b/lib/src/impl/platform_object.dart index 861478758..16ad9b2b0 100644 --- a/lib/src/impl/platform_object.dart +++ b/lib/src/impl/platform_object.dart @@ -25,8 +25,17 @@ abstract class PlatformObject { bool _registering = false; Future get handle async { + // This handle can be required simultaneously or with less time difference + // as a result of asynchronous invocations (from app code). There is + // a very high probability that the handle is not acquired from + // platform side yet, but another initiator is requesting the handle. + // `_registering` serves as a flag to avoid another method call + // to platform side. These initiators need to be server after platform + // has responded with proper handle, so a short hold and re-check will + // return the handle whenever it is available. + // 250ms is arbitrarily set as delay duration considering that is a fairly + // reasonable timeout for platform to respond. if(_registering){ - //if handle is queried while already being fetched from remote, it must be put on hold.. await Future.delayed(Duration(milliseconds: 250)); return await this.handle; } From 6582afe7139e16c2eade728630adcf10e8e34743 Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Tue, 7 Jul 2020 01:20:28 +0530 Subject: [PATCH 04/15] optimizing imports ably_implementation --- lib/src/ably_implementation.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/ably_implementation.dart b/lib/src/ably_implementation.dart index 996d5f042..337ae42b5 100644 --- a/lib/src/ably_implementation.dart +++ b/lib/src/ably_implementation.dart @@ -1,15 +1,15 @@ import 'dart:async'; +import 'package:ably_flutter_plugin/src/generated/platformconstants.dart' show PlatformMethod; import 'package:ably_flutter_plugin/src/interface.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:ably_flutter_plugin/src/generated/platformconstants.dart' show PlatformMethod; import 'package:streams_channel/streams_channel.dart'; -import 'spec/spec.dart' as spec; + import 'codec.dart'; import 'impl/platform_object.dart'; import 'impl/realtime/realtime.dart'; import 'impl/rest/rest.dart'; +import 'spec/spec.dart' as spec; /// Ably plugin implementation From c792574efd37a3cff749aa066970262d0ff6dfec Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Tue, 7 Jul 2020 15:10:08 +0530 Subject: [PATCH 05/15] improve the lazy sync handle acquisition flow and a timeout to rule it out in case we never hear back from platform side --- lib/src/ably_implementation.dart | 12 ++++++++++-- lib/src/impl/platform_object.dart | 25 +++++++++++++++++++------ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/src/ably_implementation.dart b/lib/src/ably_implementation.dart index 337ae42b5..ba94422ec 100644 --- a/lib/src/ably_implementation.dart +++ b/lib/src/ably_implementation.dart @@ -51,12 +51,20 @@ class AblyImplementation implements AblyLibrary { if(_initialized) return; _init(); // if `_initialized` is false => initialization is in progress. - // Let's wait for 250ms and retry + // Let's wait for 10ms and retry. If the total time exceeds 2 seconds, + // a TimeoutException is raised // // this is required as many asynchronous `_initialize` calls // will be invoked from different Ably instances while(true){ - await Future.delayed(Duration(milliseconds: 250)); + bool _registrationFailed = false; + Future.delayed(Duration(seconds: 2), (){ + _registrationFailed = true; + }); + await Future.delayed(Duration(milliseconds: 10)); + if(_registrationFailed){ + throw TimeoutException("Handle aquiring timed out"); + } if(_initialized) return; } } diff --git a/lib/src/impl/platform_object.dart b/lib/src/impl/platform_object.dart index 16ad9b2b0..f3c87ad9d 100644 --- a/lib/src/impl/platform_object.dart +++ b/lib/src/impl/platform_object.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:ably_flutter_plugin/src/interface.dart'; import 'package:ably_flutter_plugin/src/ably_implementation.dart'; import 'package:ably_flutter_plugin/src/impl/message.dart'; +import 'package:ably_flutter_plugin/src/interface.dart'; import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; @@ -30,14 +30,27 @@ abstract class PlatformObject { // a very high probability that the handle is not acquired from // platform side yet, but another initiator is requesting the handle. // `_registering` serves as a flag to avoid another method call - // to platform side. These initiators need to be server after platform + // to platform side. These initiators need to be served after platform // has responded with proper handle, so a short hold and re-check will // return the handle whenever it is available. - // 250ms is arbitrarily set as delay duration considering that is a fairly - // reasonable timeout for platform to respond. + // 10ms is set as delay duration based on the conversation here + // https://github.com/ably/ably-flutter/pull/18#discussion_r450699980. + // + // If the total time exceeds 2 seconds, a TimeoutException is raised if(_registering){ - await Future.delayed(Duration(milliseconds: 250)); - return await this.handle; + bool _registrationFailed = false; + Future.delayed(Duration(seconds: 2), (){ + _registrationFailed = true; + }); + while(true){ + await Future.delayed(Duration(milliseconds: 10)); + if(_registrationFailed){ + throw TimeoutException("Handle aquiring timed out"); + } + if(_handle!=null){ + return _handle; + } + } } if(_handle == null) { _registering = true; From 9a4d5178278fbbbbcf5e0f2a543104c77b88c25a Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Sat, 11 Jul 2020 23:06:11 +0530 Subject: [PATCH 06/15] API Update import 'package:ably_flutter_plugin/ably.dart' as ably; ably.Rest and ably.Realtime are now available directly to interact with instead of a scoping inside a singleton class --- example/lib/main.dart | 21 ++--- example/pubspec.lock | 1 + lib/ably.dart | 6 +- lib/src/ably_implementation.dart | 107 -------------------------- lib/src/impl/platform_object.dart | 19 +++-- lib/src/impl/realtime/channels.dart | 5 +- lib/src/impl/realtime/connection.dart | 4 +- lib/src/impl/realtime/realtime.dart | 6 +- lib/src/impl/rest/channels.dart | 5 +- lib/src/impl/rest/rest.dart | 6 +- lib/src/info.dart | 14 ++++ lib/src/interface.dart | 34 -------- lib/src/platform.dart | 57 ++++++++++++++ lib/src/spec/realtime/realtime.dart | 4 +- lib/src/spec/rest/rest.dart | 5 +- pubspec.lock | 1 + test/ably_flutter_plugin_test.dart | 25 +++--- 17 files changed, 130 insertions(+), 190 deletions(-) delete mode 100644 lib/src/ably_implementation.dart create mode 100644 lib/src/info.dart delete mode 100644 lib/src/interface.dart create mode 100644 lib/src/platform.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 02a674faf..9e1866238 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,8 +1,9 @@ -import 'package:flutter/material.dart'; import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:ably_flutter_plugin/ably.dart' as ably; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'provisioning.dart' as provisioning; void main() => runApp(MyApp()); @@ -26,8 +27,8 @@ class _MyAppState extends State { provisioning.AppKey _appKey; OpState _realtimeCreationState = OpState.NotStarted; OpState _restCreationState = OpState.NotStarted; - ably.Realtime _realtime; - ably.Rest _rest; + ably.RealtimeInterface _realtime; + ably.RestInterface _rest; ably.ConnectionState _realtimeConnectionState; @override @@ -45,12 +46,12 @@ class _MyAppState extends State { // Platform messages may fail, so we use a try/catch PlatformException. try { - platformVersion = await ably.Ably.platformVersion; + platformVersion = await ably.platformVersion(); } on PlatformException { platformVersion = 'Failed to get platform version.'; } try { - ablyVersion = await ably.Ably.version; + ablyVersion = await ably.version(); } on PlatformException { ablyVersion = 'Failed to get Ably version.'; } @@ -96,9 +97,9 @@ class _MyAppState extends State { print("Custom logger :: $msg $exception"); }; - ably.Rest rest; + ably.RestInterface rest; try{ - rest = ably.Ably.Rest(options: clientOptions); + rest = ably.Rest(options: clientOptions); } catch (error) { print('Error creating Ably Rest: ${error}'); setState(() { _restCreationState = OpState.Failed; }); @@ -135,9 +136,9 @@ class _MyAppState extends State { }; clientOptions.autoConnect = false; - ably.Realtime realtime; + ably.RealtimeInterface realtime; try { - realtime = ably.Ably.Realtime(options: clientOptions); + realtime = ably.Realtime(options: clientOptions); //One can listen from multiple listeners on the same event, // and must cancel each subscription one by one diff --git a/example/pubspec.lock b/example/pubspec.lock index 8f4d9ecbe..7182555fa 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -221,3 +221,4 @@ packages: version: "3.6.1" sdks: dart: ">=2.6.0 <3.0.0" + flutter: ">=1.17.0 <2.0.0" diff --git a/lib/ably.dart b/lib/ably.dart index aae000b7b..498c657bc 100644 --- a/lib/ably.dart +++ b/lib/ably.dart @@ -1,4 +1,6 @@ -export 'src/interface.dart'; export 'src/defaults.dart'; -export 'src/spec/spec.dart'; export 'src/generated/platformconstants.dart'; +export 'src/impl/realtime/realtime.dart'; +export 'src/impl/rest/rest.dart'; +export 'src/info.dart'; +export 'src/spec/spec.dart'; diff --git a/lib/src/ably_implementation.dart b/lib/src/ably_implementation.dart deleted file mode 100644 index ba94422ec..000000000 --- a/lib/src/ably_implementation.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:async'; - -import 'package:ably_flutter_plugin/src/generated/platformconstants.dart' show PlatformMethod; -import 'package:ably_flutter_plugin/src/interface.dart'; -import 'package:flutter/services.dart'; -import 'package:streams_channel/streams_channel.dart'; - -import 'codec.dart'; -import 'impl/platform_object.dart'; -import 'impl/realtime/realtime.dart'; -import 'impl/rest/rest.dart'; -import 'spec/spec.dart' as spec; - - -/// Ably plugin implementation -/// Single point of interaction that exposes all necessary APIs to ably objects -class AblyImplementation implements AblyLibrary { - - /// instance of method channel to interact with android/ios code - final MethodChannel methodChannel; - - /// instance of method channel to listen to android/ios events - final StreamsChannel streamsChannel; - - /// Storing all platform objects, for easy references/cleanup - final List _platformObjects = []; - - factory AblyImplementation() { - /// Uses our custom message codec so that we can pass Ably types to the - /// platform implementations. - StandardMethodCodec codec = StandardMethodCodec(Codec()); - return AblyImplementation._constructor( - MethodChannel('io.ably.flutter.plugin', codec), - StreamsChannel('io.ably.flutter.stream', codec) - ); - } - - bool _initialized = false; - - AblyImplementation._constructor(this.methodChannel, this.streamsChannel); - - void _init(){ - methodChannel.invokeMethod(PlatformMethod.registerAbly).then((_){ - _initialized = true; - }); - } - - /// Initializing ably on platform side by invoking `register` platform method. - /// Register will clear any stale instances on platform. - Future _initialize() async { - if(_initialized) return; - _init(); - // if `_initialized` is false => initialization is in progress. - // Let's wait for 10ms and retry. If the total time exceeds 2 seconds, - // a TimeoutException is raised - // - // this is required as many asynchronous `_initialize` calls - // will be invoked from different Ably instances - while(true){ - bool _registrationFailed = false; - Future.delayed(Duration(seconds: 2), (){ - _registrationFailed = true; - }); - await Future.delayed(Duration(milliseconds: 10)); - if(_registrationFailed){ - throw TimeoutException("Handle aquiring timed out"); - } - if(_initialized) return; - } - } - - Future invoke(String method, [dynamic arguments]) async { - await _initialize(); - return await methodChannel.invokeMethod(method, arguments); - } - - @override - spec.Realtime Realtime({ - spec.ClientOptions options, - final String key - }) { - // TODO options.authCallback - // TODO options.logHandler - final r = RealtimePlatformObject(options: options, key: key); - _platformObjects.add(r); - return r; - } - - @override - spec.Rest Rest({ - spec.ClientOptions options, - final String key - }) { - // TODO options.authCallback - // TODO options.logHandler - final r = RestPlatformObject(options: options, key: key); - _platformObjects.add(r); - return r; - } - - @override - Future get platformVersion async => await invoke(PlatformMethod.getPlatformVersion); - - @override - Future get version async => await invoke(PlatformMethod.getVersion); - -} diff --git a/lib/src/impl/platform_object.dart b/lib/src/impl/platform_object.dart index f3c87ad9d..cf55bcc08 100644 --- a/lib/src/impl/platform_object.dart +++ b/lib/src/impl/platform_object.dart @@ -1,8 +1,7 @@ import 'dart:async'; -import 'package:ably_flutter_plugin/src/ably_implementation.dart'; import 'package:ably_flutter_plugin/src/impl/message.dart'; -import 'package:ably_flutter_plugin/src/interface.dart'; +import 'package:ably_flutter_plugin/src/platform.dart' as platform; import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; @@ -60,22 +59,26 @@ abstract class PlatformObject { return _handle; } - AblyImplementation get _ablyPlugin => (Ably as AblyImplementation); - MethodChannel get methodChannel => _ablyPlugin.methodChannel; - StreamsChannel get eventChannel => _ablyPlugin.streamsChannel; + MethodChannel get methodChannel => platform.methodChannel; + StreamsChannel get eventChannel => platform.streamsChannel; static Future dispose() async { //TODO implement or convert to abstract! return null; } - /// Call a method. - Future invoke(final String method, [final dynamic argument]) async { + /// invoke platform method channel without AblyMessage encapsulation + Future invokeRaw(final String method, [final dynamic arguments]) async { + return await platform.invoke(method, arguments); + } + + /// invoke platform method channel with AblyMessage encapsulation + Future invoke(final String method, [final dynamic argument]) async { int _handle = await handle; final message = (null != argument) ? AblyMessage(AblyMessage(argument, handle: _handle)) : AblyMessage(_handle); - return await Ably.invoke(method, message); + return await invokeRaw(method, message); } Future> _listen(final String method) async { diff --git a/lib/src/impl/realtime/channels.dart b/lib/src/impl/realtime/channels.dart index 63331f9d4..4f7587e90 100644 --- a/lib/src/impl/realtime/channels.dart +++ b/lib/src/impl/realtime/channels.dart @@ -5,6 +5,7 @@ import 'package:ably_flutter_plugin/src/impl/realtime/realtime.dart'; import 'package:ably_flutter_plugin/src/spec/push/channels.dart'; import 'package:ably_flutter_plugin/src/spec/spec.dart' as spec; import 'package:flutter/services.dart'; + import '../platform_object.dart'; @@ -24,7 +25,7 @@ class RealtimePlatformChannel extends PlatformObject implements spec.RealtimeCha RealtimePlatformChannel(this.ably, this.name, this.options): super(); - RealtimePlatformObject get realtimePlatformObject => this.ably as RealtimePlatformObject; + Realtime get realtimePlatformObject => this.ably as Realtime; /// createPlatformInstance will return realtimePlatformObject's handle /// as that is what will be required in platforms end to find realtime instance @@ -121,7 +122,7 @@ class RealtimePlatformChannel extends PlatformObject implements spec.RealtimeCha class RealtimePlatformChannels extends spec.RealtimeChannels{ - RealtimePlatformChannels(RealtimePlatformObject ably): super(ably); + RealtimePlatformChannels(Realtime ably): super(ably); @override RealtimePlatformChannel createChannel(name, options){ diff --git a/lib/src/impl/realtime/connection.dart b/lib/src/impl/realtime/connection.dart index 826c528ab..e9ca255fb 100644 --- a/lib/src/impl/realtime/connection.dart +++ b/lib/src/impl/realtime/connection.dart @@ -3,13 +3,13 @@ import 'dart:async'; import 'package:ably_flutter_plugin/ably.dart'; import 'package:ably_flutter_plugin/src/impl/realtime/realtime.dart'; -import '../platform_object.dart'; import '../../spec/spec.dart' show Connection, ConnectionState, ErrorInfo; +import '../platform_object.dart'; class ConnectionPlatformObject extends PlatformObject implements Connection { - RealtimePlatformObject realtimePlatformObject; + Realtime realtimePlatformObject; ConnectionPlatformObject(this.realtimePlatformObject); diff --git a/lib/src/impl/realtime/realtime.dart b/lib/src/impl/realtime/realtime.dart index e242d7f4d..3c061199d 100644 --- a/lib/src/impl/realtime/realtime.dart +++ b/lib/src/impl/realtime/realtime.dart @@ -8,9 +8,9 @@ import '../platform_object.dart'; import 'connection.dart'; -class RealtimePlatformObject extends PlatformObject implements spec.Realtime { +class Realtime extends PlatformObject implements spec.RealtimeInterface { - RealtimePlatformObject({ + Realtime({ ClientOptions options, final String key }) : @@ -22,7 +22,7 @@ class RealtimePlatformObject extends PlatformObject implements spec.Realtime { } @override - Future createPlatformInstance() async => await Ably.invoke( + Future createPlatformInstance() async => await invokeRaw( PlatformMethod.createRealtimeWithOptions, AblyMessage(options) ); diff --git a/lib/src/impl/rest/channels.dart b/lib/src/impl/rest/channels.dart index 1426368e5..96060b789 100644 --- a/lib/src/impl/rest/channels.dart +++ b/lib/src/impl/rest/channels.dart @@ -4,6 +4,7 @@ import 'package:ably_flutter_plugin/ably.dart'; import 'package:ably_flutter_plugin/src/impl/rest/rest.dart'; import 'package:ably_flutter_plugin/src/spec/spec.dart' as spec; import 'package:flutter/services.dart'; + import '../platform_object.dart'; @@ -23,7 +24,7 @@ class RestPlatformChannel extends PlatformObject implements spec.RestChannel{ RestPlatformChannel(this.ably, this.name, this.options); - RestPlatformObject get restPlatformObject => this.ably as RestPlatformObject; + Rest get restPlatformObject => this.ably as Rest; /// createPlatformInstance will return restPlatformObject's handle /// as that is what will be required in platforms end to find rest instance @@ -54,7 +55,7 @@ class RestPlatformChannel extends PlatformObject implements spec.RestChannel{ class RestPlatformChannels extends spec.RestChannels{ - RestPlatformChannels(RestPlatformObject ably): super(ably); + RestPlatformChannels(Rest ably): super(ably); @override RestPlatformChannel createChannel(name, options){ diff --git a/lib/src/impl/rest/rest.dart b/lib/src/impl/rest/rest.dart index 836f02ff8..4b83757a5 100644 --- a/lib/src/impl/rest/rest.dart +++ b/lib/src/impl/rest/rest.dart @@ -8,9 +8,9 @@ import '../platform_object.dart'; import 'channels.dart'; -class RestPlatformObject extends PlatformObject implements spec.Rest { +class Rest extends PlatformObject implements spec.RestInterface { - RestPlatformObject({ + Rest({ ClientOptions options, final String key }) : @@ -21,7 +21,7 @@ class RestPlatformObject extends PlatformObject implements spec.Rest createPlatformInstance() async => await Ably.invoke( + Future createPlatformInstance() async => await invokeRaw( PlatformMethod.createRestWithOptions, AblyMessage(options) ); diff --git a/lib/src/info.dart b/lib/src/info.dart new file mode 100644 index 000000000..a68ab4f31 --- /dev/null +++ b/lib/src/info.dart @@ -0,0 +1,14 @@ +import 'package:ably_flutter_plugin/src/generated/platformconstants.dart' show PlatformMethod; + +import 'platform.dart' show invoke; + + +/// Get android/iOS platform version +Future platformVersion() async { + return await invoke(PlatformMethod.getPlatformVersion); +} + +/// Get ably library version +Future version() async { + return await invoke(PlatformMethod.getVersion); +} diff --git a/lib/src/interface.dart b/lib/src/interface.dart deleted file mode 100644 index 83a6a70e1..000000000 --- a/lib/src/interface.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'ably_implementation.dart'; -import 'spec/spec.dart' as spec; - -abstract class AblyLibrary { - - static final AblyLibrary _instance = AblyImplementation(); - factory AblyLibrary() => _instance; - - /// Invokes a platform method - Future invoke(String method, [dynamic arguments]); - - /// Returns platform version - Future get platformVersion; - - /// Returns ably library version - Future get version; - - /// Creates a [Realtime] instance either with [options] or with [key] - /// obtained from Ably dashboard - spec.Realtime Realtime({ - spec.ClientOptions options, - final String key - }); - - /// Creates a [Rest] instance either with [options] or with [key] - /// obtained from Ably dashboard - spec.Rest Rest({ - spec.ClientOptions options, - final String key - }); - -} - -final AblyLibrary Ably = AblyLibrary(); diff --git a/lib/src/platform.dart b/lib/src/platform.dart new file mode 100644 index 000000000..be883e64b --- /dev/null +++ b/lib/src/platform.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:ably_flutter_plugin/src/generated/platformconstants.dart' show PlatformMethod; +import 'package:flutter/services.dart'; +import 'package:streams_channel/streams_channel.dart'; + +import 'codec.dart'; + + +/// instance of [StandardMethodCodec] with custom [MessageCodec] for +/// exchanging Ably types with platform via platform channels +/// viz., [MethodChannel] and [StreamsChannel] +StandardMethodCodec codec = StandardMethodCodec(Codec()); + +/// instance of method channel to interact with android/ios code +final MethodChannel methodChannel = MethodChannel('io.ably.flutter.plugin', codec); + +/// instance of method channel to listen to android/ios events +final StreamsChannel streamsChannel = StreamsChannel('io.ably.flutter.stream', codec); + + +bool _initialized = false; + +void _init(){ + methodChannel.invokeMethod(PlatformMethod.registerAbly).then((_){ + _initialized = true; + }); +} + +/// Initializing ably on platform side by invoking `register` platform method. +/// Register will clear any stale instances on platform. +Future _initialize() async { + if(_initialized) return; + _init(); + // if `_initialized` is false => initialization is in progress. + // Let's wait for 10ms and retry. If the total time exceeds 2 seconds, + // a TimeoutException is raised + // + // this is required as many asynchronous `_initialize` calls + // will be invoked from different Ably instances + while(true){ + bool _registrationFailed = false; + Future.delayed(Duration(seconds: 2), (){ + _registrationFailed = true; + }); + await Future.delayed(Duration(milliseconds: 10)); + if(_registrationFailed){ + throw TimeoutException("Handle aquiring timed out"); + } + if(_initialized) return; + } +} + +Future invoke(String method, [dynamic arguments]) async { + await _initialize(); + return await methodChannel.invokeMethod(method, arguments); +} diff --git a/lib/src/spec/realtime/realtime.dart b/lib/src/spec/realtime/realtime.dart index dee810e13..b640f9969 100644 --- a/lib/src/spec/realtime/realtime.dart +++ b/lib/src/spec/realtime/realtime.dart @@ -4,9 +4,9 @@ import '../rest/options.dart'; import 'channels.dart'; -abstract class Realtime extends AblyBase { +abstract class RealtimeInterface extends AblyBase { - Realtime({ + RealtimeInterface({ ClientOptions options, final String key }): connection=null, //To be assigned as required on implementation diff --git a/lib/src/spec/rest/rest.dart b/lib/src/spec/rest/rest.dart index 93a379ffd..69bc346b9 100644 --- a/lib/src/spec/rest/rest.dart +++ b/lib/src/spec/rest/rest.dart @@ -1,7 +1,6 @@ //import 'package:flutter/foundation.dart'; import '../auth.dart'; -//import '../common.dart'; import '../push/push.dart'; import '../rest/ably_base.dart'; import '../rest/options.dart'; @@ -10,13 +9,13 @@ import 'channels.dart'; import 'options.dart'; -abstract class Rest extends AblyBase { +abstract class RestInterface extends AblyBase { Auth auth; Push push; C channels; - Rest({ + RestInterface({ ClientOptions options, final String key }): super(options: options, key: key); diff --git a/pubspec.lock b/pubspec.lock index 097821d8e..6d9a0eeb3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -431,3 +431,4 @@ packages: version: "2.2.0" sdks: dart: ">=2.6.0 <3.0.0" + flutter: ">=1.17.0 <2.0.0" diff --git a/test/ably_flutter_plugin_test.dart b/test/ably_flutter_plugin_test.dart index f79e96789..78adee055 100644 --- a/test/ably_flutter_plugin_test.dart +++ b/test/ably_flutter_plugin_test.dart @@ -1,6 +1,7 @@ import 'package:ably_flutter_plugin/ably.dart'; -import 'package:ably_flutter_plugin/src/ably_implementation.dart'; import 'package:ably_flutter_plugin/src/impl/rest/rest.dart'; +import 'package:ably_flutter_plugin/src/info.dart'; +import 'package:ably_flutter_plugin/src/platform.dart' as platform; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -15,14 +16,14 @@ extension on PlatformMethod { void main() { - MethodChannel channel = (Ably as AblyImplementation).methodChannel; + MethodChannel channel = platform.methodChannel; TestWidgetsFlutterBinding.ensureInitialized(); int counter = 0; //test constants - String platformVersion = '42'; - String nativeLibraryVersion = '1.1.0'; + String _platformVersion = '42'; + String _nativeLibraryVersion = '1.1.0'; setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { @@ -31,9 +32,9 @@ void main() { return true; case PlatformMethod.getPlatformVersion: - return platformVersion; + return _platformVersion; case PlatformMethod.getVersion: - return nativeLibraryVersion; + return _nativeLibraryVersion; case "createrestWithKey": case PlatformMethod.createRestWithOptions: @@ -53,38 +54,38 @@ void main() { }); test(PlatformMethod.getPlatformVersion, () async { - expect(await Ably.platformVersion, platformVersion); + expect(await platformVersion(), _platformVersion); }); test(PlatformMethod.getVersion, () async { - expect(await Ably.version, nativeLibraryVersion); + expect(await version(), _nativeLibraryVersion); }); test(PlatformMethod.createRestWithOptions, () async { ClientOptions o = ClientOptions(); String host = "http://rest.ably.io/"; o.restHost = host; - RestPlatformObject rest = Ably.Rest(options: o); + Rest rest = Rest(options: o); expect(await rest.handle, counter); expect(rest.options.restHost, host); }); test("createRestWithToken", () async { String key = 'TEST-KEY'; - RestPlatformObject rest = Ably.Rest(key: key); + Rest rest = Rest(key: key); expect(await rest.handle, counter); expect(rest.options.tokenDetails.token, key); }); test("createRestWithKey", () async { String key = 'TEST:KEY'; - RestPlatformObject rest = Ably.Rest(key: key); + Rest rest = Rest(key: key); expect(await rest.handle, counter); expect(rest.options.key, key); }); test("publishMessage", () async { - RestPlatformObject rest = Ably.Rest(key: 'TEST-KEY'); + Rest rest = Rest(key: 'TEST-KEY'); await rest.channels.get('test').publish(name: 'name', data: 'data'); expect(1, 1); }); From e81ffcbaaf06ba286969cb0af4469030eb5a071d Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Sat, 11 Jul 2020 23:09:59 +0530 Subject: [PATCH 07/15] update usage in README --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a8668a154..0b2295640 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,6 @@ site to discuss privately. import 'package:ably_flutter_plugin/ably.dart' as ably; ``` -##### Accessing Ably instance - -Ably library instance can be accessed as `ably.Ably` - ##### create a ClientOptions ```dart @@ -55,7 +51,7 @@ clientOptions.logLevel = ably.LogLevel.verbose; //optional ##### Rest API ```dart -ably.Rest rest = ably.Ably.Rest(options: clientOptions); +ably.Rest rest = ably.Rest(options: clientOptions); ``` Getting a channel instance @@ -84,7 +80,7 @@ publishMessages(); ##### Realtime API ```dart -ably.Realtime realtime = ably.Ably.Realtime(options: clientOptions); +ably.Realtime realtime = ably.Realtime(options: clientOptions); ``` Listen to connection state change event From 668f32eb782455f389528f5ec0cfdd21cfefa54d Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Sun, 12 Jul 2020 23:19:37 +0530 Subject: [PATCH 08/15] Slazy + Hot-Reload fix on iOS ## Sync + Lazy = Slazy!!! 1. Singleton AblyFlutter instance on iOS side ## Hot-reload issue 2. register ably is now used to dispose off any registered platform instances 3. StreamsChannel source is duplicated on iOS side and necessary changes are made to make hot-reload work well --- ios/Classes/AblyFlutter.h | 3 + ios/Classes/AblyFlutter.m | 27 ++--- ios/Classes/AblyFlutterMessage.h | 4 +- ios/Classes/AblyFlutterMessage.m | 5 +- ios/Classes/AblyFlutterPlugin.h | 2 +- ios/Classes/AblyFlutterPlugin.m | 101 +++++------------ ios/Classes/AblyFlutterStreamHandler.h | 7 +- ios/Classes/AblyFlutterStreamHandler.m | 21 ++-- ios/Classes/AblyFlutterSurfaceRealtime.h | 20 ---- ios/Classes/AblyFlutterSurfaceRealtime.m | 30 ----- ios/Classes/AblyStreamsChannel.h | 26 +++++ ios/Classes/AblyStreamsChannel.m | 138 +++++++++++++++++++++++ ios/Classes/codec/AblyFlutterReader.m | 2 +- 13 files changed, 225 insertions(+), 161 deletions(-) delete mode 100644 ios/Classes/AblyFlutterSurfaceRealtime.h delete mode 100644 ios/Classes/AblyFlutterSurfaceRealtime.m create mode 100644 ios/Classes/AblyStreamsChannel.h create mode 100644 ios/Classes/AblyStreamsChannel.m diff --git a/ios/Classes/AblyFlutter.h b/ios/Classes/AblyFlutter.h index e158dba1d..1d9647433 100644 --- a/ios/Classes/AblyFlutter.h +++ b/ios/Classes/AblyFlutter.h @@ -8,6 +8,8 @@ NS_ASSUME_NONNULL_BEGIN @interface AblyFlutter : NSObject ++ (id)instance; + -(NSNumber *)createRestWithOptions:(ARTClientOptions *)options; -(nullable ARTRest *)getRest:(NSNumber *)handle; @@ -24,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN platform objects have been closed down cleanly. */ -(void)disposeWithCompletionHandler:(dispatch_block_t)completionHandler; +-(void)dispose; @end diff --git a/ios/Classes/AblyFlutter.m b/ios/Classes/AblyFlutter.m index e8f070fbc..735bfdc68 100644 --- a/ios/Classes/AblyFlutter.m +++ b/ios/Classes/AblyFlutter.m @@ -6,12 +6,21 @@ @implementation AblyFlutter { - BOOL _disposed; NSMutableDictionary* _realtimeInstances; NSMutableDictionary* _restInstances; long long _nextHandle; } ++ (id)instance { + static AblyFlutter *sharedInstance = nil; + @synchronized(self) { + if (sharedInstance == nil) { + sharedInstance = [[self alloc] init]; + } + } + return sharedInstance; +} + -(instancetype)init { self = [super init]; if (!self) { @@ -25,19 +34,11 @@ -(instancetype)init { return self; } -#define ASSERT_NOT_DISPOSED \ - if (_disposed) { \ - [NSException raise:NSInternalInconsistencyException \ - format:@"Instance disposed."]; \ - } - -(NSNumber *)createRestWithOptions:(ARTClientOptions *const)options { if (!options) { [NSException raise:NSInvalidArgumentException format:@"options cannot be nil."]; } - ASSERT_NOT_DISPOSED - ARTRest *const instance = [[ARTRest alloc] initWithOptions:options]; NSNumber *const handle = @(_nextHandle++); [_restInstances setObject:instance forKey:handle]; @@ -53,8 +54,6 @@ -(NSNumber *)createRealtimeWithOptions:(ARTClientOptions *const)options { [NSException raise:NSInvalidArgumentException format:@"options cannot be nil."]; } - ASSERT_NOT_DISPOSED - ARTRealtime *const instance = [[ARTRealtime alloc] initWithOptions:options]; NSNumber *const handle = @(_nextHandle++); [_realtimeInstances setObject:instance forKey:handle]; @@ -70,15 +69,12 @@ -(void)disposeWithCompletionHandler:(const dispatch_block_t)completionHandler { [NSException raise:NSInvalidArgumentException format:@"completionHandler cannot be nil."]; } - ASSERT_NOT_DISPOSED - // TODO upgrade iOS runtime requirement to 10.0 so we can use this: // dispatch_assert_queue(dispatch_get_main_queue()); // This is contrived for now but the point is that we can introduce a clean, // asynchronous close via a background queue here if required. - dispatch_async(dispatch_get_main_queue(), ^ - { + dispatch_async(dispatch_get_main_queue(), ^{ [self dispose]; completionHandler(); }); @@ -89,6 +85,7 @@ -(void)dispose { [r close]; } [_realtimeInstances removeAllObjects]; + [_restInstances removeAllObjects]; } @end diff --git a/ios/Classes/AblyFlutterMessage.h b/ios/Classes/AblyFlutterMessage.h index 0955312f8..496e85b54 100644 --- a/ios/Classes/AblyFlutterMessage.h +++ b/ios/Classes/AblyFlutterMessage.h @@ -7,8 +7,8 @@ NS_ASSUME_NONNULL_BEGIN +(instancetype)new NS_UNAVAILABLE; -(instancetype)init NS_UNAVAILABLE; --(instancetype)initWithHandle:(NSNumber *)handle - message:(id)message NS_DESIGNATED_INITIALIZER; +-(instancetype)initWithMessage:(id)message + handle:(NSNumber *)handle NS_DESIGNATED_INITIALIZER; @property(nonatomic, readonly) NSNumber * handle; @property(nonatomic, readonly) id message; diff --git a/ios/Classes/AblyFlutterMessage.m b/ios/Classes/AblyFlutterMessage.m index 98ddab118..54980727f 100644 --- a/ios/Classes/AblyFlutterMessage.m +++ b/ios/Classes/AblyFlutterMessage.m @@ -5,10 +5,7 @@ @implementation AblyFlutterMessage @synthesize handle = _handle; @synthesize message = _message; --(instancetype)initWithHandle:(NSNumber *const)handle message:(const id)message { - if (!handle) { - [NSException raise:NSInvalidArgumentException format:@"handle cannot be nil."]; - } +-(instancetype)initWithMessage:(const id)message handle:(NSNumber *const)handle { if (!message) { [NSException raise:NSInvalidArgumentException format:@"message cannot be nil."]; } diff --git a/ios/Classes/AblyFlutterPlugin.h b/ios/Classes/AblyFlutterPlugin.h index b4df808a9..5c10ea8e2 100644 --- a/ios/Classes/AblyFlutterPlugin.h +++ b/ios/Classes/AblyFlutterPlugin.h @@ -8,7 +8,7 @@ NS_ASSUME_NONNULL_BEGIN +(instancetype)new NS_UNAVAILABLE; +(instancetype)init NS_UNAVAILABLE; -- (AblyFlutter *)ablyWithHandle:(NSNumber *)handle; +@property(nonatomic, readonly) AblyFlutter * ably; @end diff --git a/ios/Classes/AblyFlutterPlugin.m b/ios/Classes/AblyFlutterPlugin.m index ce296a9c9..f01244674 100644 --- a/ios/Classes/AblyFlutterPlugin.m +++ b/ios/Classes/AblyFlutterPlugin.m @@ -8,7 +8,7 @@ #import "AblyFlutterMessage.h" #import "AblyFlutter.h" #import "AblyFlutterStreamHandler.h" -#import "FlutterStreamsChannel.h" +#import "AblyStreamsChannel.h" #import "codec/AblyPlatformConstants.h" #define LOG(fmt, ...) NSLog((@"%@:%d " fmt), [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, ##__VA_ARGS__) @@ -24,7 +24,6 @@ */ @interface AblyFlutterPlugin () -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; --(nullable AblyFlutter *)ablyWithHandle:(NSNumber *)handle; @end NS_ASSUME_NONNULL_END @@ -43,16 +42,13 @@ -(nullable AblyFlutter *)ablyWithHandle:(NSNumber *)handle; static FlutterHandler _createRestWithOptions = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { AblyFlutterMessage *const message = call.arguments; - LOG(@"message for handle %@", message.handle); - AblyFlutter *const ably = [plugin ablyWithHandle:message.handle]; - // TODO if ably is nil here then an error response, perhaps? or allow Dart side to understand null response? + AblyFlutter *const ably = [plugin ably]; result([ably createRestWithOptions:message.message]); }; static FlutterHandler _publishRestMessage = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { AblyFlutterMessage *const message = call.arguments; - LOG(@"message for handle %@", message.handle); - AblyFlutter *const ably = [plugin ablyWithHandle:message.handle]; + AblyFlutter *const ably = [plugin ably]; AblyFlutterMessage *const messageData = message.message; NSMutableDictionary* _dataMap = messageData.message; NSString *channelName = (NSString*)[_dataMap objectForKey:@"channel"]; @@ -77,17 +73,13 @@ -(nullable AblyFlutter *)ablyWithHandle:(NSNumber *)handle; static FlutterHandler _createRealtimeWithOptions = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { AblyFlutterMessage *const message = call.arguments; - LOG(@"message for handle %@", message.handle); - AblyFlutter *const ably = [plugin ablyWithHandle:message.handle]; - // TODO if ably is nil here then an error response, perhaps? or allow Dart side to understand null response? + AblyFlutter *const ably = [plugin ably]; result([ably createRealtimeWithOptions:message.message]); }; static FlutterHandler _connectRealtime = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { AblyFlutterMessage *const message = call.arguments; - LOG(@"message for handle %@", message.handle); - AblyFlutter *const ably = [plugin ablyWithHandle:message.handle]; - // TODO if ably is nil here then an error response, perhaps? or allow Dart side to understand null response? + AblyFlutter *const ably = [plugin ably]; NSNumber *const handle = message.message; [[ably realtimeWithHandle:handle] connect]; result(nil); @@ -95,8 +87,7 @@ -(nullable AblyFlutter *)ablyWithHandle:(NSNumber *)handle; static FlutterHandler _closeRealtime = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { AblyFlutterMessage *const message = call.arguments; - LOG(@"message for handle %@", message.handle); - AblyFlutter *const ably = [plugin ablyWithHandle:message.handle]; + AblyFlutter *const ably = [plugin ably]; NSNumber *const handle = message.message; [[ably realtimeWithHandle:handle] close]; result(nil); @@ -105,35 +96,43 @@ -(nullable AblyFlutter *)ablyWithHandle:(NSNumber *)handle; @implementation AblyFlutterPlugin { long long _nextRegistration; NSDictionary* _handlers; - NSNumber* _ablyHandle; - AblyFlutter* _ably; + AblyStreamsChannel* _streamsChannel; FlutterMethodChannel* _channel; } +@synthesize ably = _ably; + +(void)registerWithRegistrar:(NSObject*)registrar { LOG(@"registrar: %@", [registrar class]); + + // Initializing reader writer and method codecs FlutterStandardReaderWriter *const readerWriter = [AblyFlutterReaderWriter new]; FlutterStandardMethodCodec *const methodCodec = [FlutterStandardMethodCodec codecWithReaderWriter:readerWriter]; - FlutterMethodChannel *const channel = - [FlutterMethodChannel methodChannelWithName:@"io.ably.flutter.plugin" binaryMessenger:[registrar messenger] codec:methodCodec]; - AblyFlutterPlugin *const plugin = - [[AblyFlutterPlugin alloc] initWithChannel:channel]; + // initializing event channel for event listeners + AblyStreamsChannel *const streamsChannel = [AblyStreamsChannel streamsChannelWithName:@"io.ably.flutter.stream" binaryMessenger:registrar.messenger codec: methodCodec]; + + // initializing method channel for round-trip method calls + FlutterMethodChannel *const channel = [FlutterMethodChannel methodChannelWithName:@"io.ably.flutter.plugin" binaryMessenger:[registrar messenger] codec:methodCodec]; + AblyFlutterPlugin *const plugin = [[AblyFlutterPlugin alloc] initWithChannel:channel streamsChannel: streamsChannel]; + // regustering method channel with registrar [registrar addMethodCallDelegate:plugin channel:channel]; - - FlutterStreamsChannel *const eventChannel = [FlutterStreamsChannel streamsChannelWithName:@"io.ably.flutter.stream" binaryMessenger:registrar.messenger codec: methodCodec]; - [eventChannel setStreamHandlerFactory:^NSObject *(id arguments) { - return [[AblyFlutterStreamHandler alloc] initWithAbly: plugin]; + + // setting up stream handler factory for eventChannel to handle multiple listeners + [streamsChannel setStreamHandlerFactory:^NSObject *(id arguments) { + return [AblyFlutterStreamHandler new]; }]; } --(instancetype)initWithChannel:(FlutterMethodChannel *const)channel { +-(instancetype)initWithChannel:(FlutterMethodChannel *const)channel streamsChannel:(AblyStreamsChannel *const)streamsChannel { self = [super init]; if (!self) { return nil; } + _ably = [AblyFlutter instance]; + self->_streamsChannel = streamsChannel; _handlers = @{ AblyPlatformMethod_getPlatformVersion: _getPlatformVersion, @@ -166,52 +165,12 @@ -(void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { -(void)registerWithCompletionHandler:(const FlutterResult)completionHandler { if (!completionHandler) { - [NSException raise:NSInvalidArgumentException - format:@"completionHandler cannot be nil."]; - } - - if (_ably && !_ablyHandle) { - // TODO could this ever happen and, in which case, do we need to cater for it? - [NSException raise:NSInternalInconsistencyException - format:@"Registration request received when still not finished processing last request."]; - } - - // TODO upgrade iOS runtime requirement to 10.0 so we can use this: - // dispatch_assert_queue(dispatch_get_main_queue()); - - NSNumber *const handle = @(_nextRegistration++); - - if (_ablyHandle) { - LOG(@"Disposing of previous Ably instance (# %@).", _ablyHandle); + [NSException raise:NSInvalidArgumentException format:@"completionHandler cannot be nil."]; } - - // Setting _ablyHandle to nil when _ably is not nil indicates that we're - // in the process of asynchronously disposing of the old instance. - _ablyHandle = nil; - - const dispatch_block_t createNew = ^ - { - LOG(@"Creating new Ably instance (# %@).", handle); - self->_ably = [AblyFlutter new]; - self->_ablyHandle = handle; - completionHandler(handle); - }; - - if (_ably) { - [_ably disposeWithCompletionHandler:^ - { - createNew(); - }]; - } else { - createNew(); - } -} - --(AblyFlutter *)ablyWithHandle:(NSNumber *)handle { - if (![handle isEqualToNumber:_ablyHandle]) { - return nil; - } - return _ably; + [_ably disposeWithCompletionHandler:^{ + [self->_streamsChannel reset]; + completionHandler(nil); + }]; } @end diff --git a/ios/Classes/AblyFlutterStreamHandler.h b/ios/Classes/AblyFlutterStreamHandler.h index faabc6468..1fedd1ea5 100644 --- a/ios/Classes/AblyFlutterStreamHandler.h +++ b/ios/Classes/AblyFlutterStreamHandler.h @@ -6,12 +6,7 @@ NS_ASSUME_NONNULL_BEGIN @interface AblyFlutterStreamHandler : NSObject -+(instancetype)new NS_UNAVAILABLE; --(instancetype)init NS_UNAVAILABLE; - --(instancetype)initWithAbly:(AblyFlutterPlugin *const)plugin NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, readonly) AblyFlutterPlugin * plugin; +@property(nonatomic, readonly) AblyFlutter * ably; - (nullable FlutterError *)onListenWithArguments:(nullable id)arguments eventSink:(FlutterEventSink)eventSink; - (nullable FlutterError *)onCancelWithArguments:(nullable id)arguments; diff --git a/ios/Classes/AblyFlutterStreamHandler.m b/ios/Classes/AblyFlutterStreamHandler.m index 3dbb6a60d..0a3550d54 100644 --- a/ios/Classes/AblyFlutterStreamHandler.m +++ b/ios/Classes/AblyFlutterStreamHandler.m @@ -8,10 +8,10 @@ @implementation AblyFlutterStreamHandler{ ARTEventListener *listener; } -@synthesize plugin = _plugin; +@synthesize ably = _ably; -- (instancetype)initWithAbly:(AblyFlutterPlugin *const)plugin { - _plugin = plugin; +- (instancetype)init{ + _ably = [AblyFlutter instance]; listener = [ARTEventListener new]; return self; } @@ -27,28 +27,27 @@ - (nullable FlutterError *)onCancelWithArguments:(nullable id)arguments { } - (void) startListening:(AblyFlutterMessage *const)message emitter:(FlutterEventSink)emitter { - AblyFlutter *const ably = [_plugin ablyWithHandle: message.handle]; AblyFlutterMessage *const _message = message.message; NSString *const eventName = _message.message; + NSNumber *const handle = _message.handle; if([AblyPlatformMethod_onRealtimeConnectionStateChanged isEqual: eventName]) { - listener = [[ably realtimeWithHandle: message.handle].connection on: ^(ARTConnectionStateChange * const stateChange) { + ARTRealtime* realtimeWithHandle = [_ably realtimeWithHandle: handle]; + listener = [realtimeWithHandle.connection on: ^(ARTConnectionStateChange * const stateChange) { emitter(stateChange); }]; - } else if([AblyPlatformMethod_onRealtimeChannelStateChanged isEqual: eventName]) { - + } else { + emitter([FlutterError errorWithCode:@"error" message:[NSString stringWithFormat:@"Invalid event name: %@", eventName] details:nil]); } } - (void) cancelListening:(AblyFlutterMessage *const)message { - AblyFlutter *const ably = [_plugin ablyWithHandle: message.handle]; AblyFlutterMessage *const _message = message.message; NSString *const eventName = _message.message; + NSNumber *const handle = _message.handle; if([AblyPlatformMethod_onRealtimeConnectionStateChanged isEqual: eventName]) { - [[ably realtimeWithHandle: message.handle].connection off: listener]; - } else if([AblyPlatformMethod_onRealtimeChannelStateChanged isEqual: eventName]) { - + [[_ably realtimeWithHandle: handle].connection off: listener]; } } diff --git a/ios/Classes/AblyFlutterSurfaceRealtime.h b/ios/Classes/AblyFlutterSurfaceRealtime.h deleted file mode 100644 index c5b93a854..000000000 --- a/ios/Classes/AblyFlutterSurfaceRealtime.h +++ /dev/null @@ -1,20 +0,0 @@ -@import Foundation; - -@class ARTRealtime; - -NS_ASSUME_NONNULL_BEGIN - -@interface AblyFlutterSurfaceRealtime : NSObject - -+(instancetype)new NS_UNAVAILABLE; --(instancetype)init NS_UNAVAILABLE; - --(instancetype)initWithInstance:(ARTRealtime *)realtime; - --(void)connect; - --(void)close; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/Classes/AblyFlutterSurfaceRealtime.m b/ios/Classes/AblyFlutterSurfaceRealtime.m deleted file mode 100644 index cced97f43..000000000 --- a/ios/Classes/AblyFlutterSurfaceRealtime.m +++ /dev/null @@ -1,30 +0,0 @@ -#import "AblyFlutterSurfaceRealtime.h" - -// TODO work out why importing Ably as a module does not work like this: -// @import Ably; -#import "Ably.h" - -@implementation AblyFlutterSurfaceRealtime { - ARTRealtime* _instance; -} - --(instancetype)initWithInstance:(ARTRealtime *)instance { - self = [super init]; - if (!self) { - return nil; - } - - _instance = instance; - - return self; -} - --(void)connect { - [_instance connect]; -} - --(void)close { - [_instance close]; -} - -@end diff --git a/ios/Classes/AblyStreamsChannel.h b/ios/Classes/AblyStreamsChannel.h new file mode 100644 index 000000000..0d9aa72dd --- /dev/null +++ b/ios/Classes/AblyStreamsChannel.h @@ -0,0 +1,26 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +#import + +//typedef FlutterMessageHandler (^FlutterStreamsHandlerFactory)(); + +@interface AblyStreamsChannel : NSObject + ++ (nonnull instancetype)streamsChannelWithName:(NSString* _Nonnull)name + binaryMessenger:(NSObject* _Nonnull)messenger; + ++ (nonnull instancetype)streamsChannelWithName:(NSString* _Nonnull)name + binaryMessenger:(NSObject* _Nonnull)messenger + codec:(NSObject* _Nonnull)codec; + +- (nonnull instancetype)initWithName:(NSString* _Nonnull)name + binaryMessenger:(NSObject* _Nonnull)messenger + codec:(NSObject* _Nonnull)codec; + +- (void)setStreamHandlerFactory:(NSObject* _Nullable (^ _Nonnull)(id _Nonnull))factory; + +- (void) reset; + +@end + diff --git a/ios/Classes/AblyStreamsChannel.m b/ios/Classes/AblyStreamsChannel.m new file mode 100644 index 000000000..a445d3b37 --- /dev/null +++ b/ios/Classes/AblyStreamsChannel.m @@ -0,0 +1,138 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +#import "AblyStreamsChannel.h" + +@interface AblyStreamsChannelStream : NSObject + @property(strong, nonatomic) FlutterEventSink sink; + @property(strong, nonatomic) NSObject *handler; +@end + +@implementation AblyStreamsChannelStream + +@end + +// Inspired from: https://github.com/flutter/engine/blob/master/shell/platform/darwin/common/framework/Source/FlutterChannels.mm +@implementation AblyStreamsChannel { + NSObject* _messenger; + NSString* _name; + NSObject* _codec; + __block NSMutableDictionary *_streams; + __block NSMutableDictionary *_listenerArguments; +} + ++ (instancetype)streamsChannelWithName:(NSString* _Nonnull)name + binaryMessenger:(NSObject* _Nonnull)messenger { + NSObject* codec = [FlutterStandardMethodCodec sharedInstance]; + return [AblyStreamsChannel streamsChannelWithName:name binaryMessenger:messenger codec:codec]; +} + ++ (instancetype)streamsChannelWithName:(NSString* _Nonnull)name + binaryMessenger:(NSObject* _Nonnull)messenger + codec:(NSObject* _Nonnull)codec { + return [[AblyStreamsChannel alloc] initWithName:name binaryMessenger:messenger codec:codec]; +} + +- (instancetype)initWithName:(NSString* _Nonnull)name + binaryMessenger:(NSObject* _Nonnull)messenger + codec:(NSObject* _Nonnull)codec { + self = [super init]; + NSAssert(self, @"Super init cannot be nil"); + _name = name; + _messenger = messenger; + _codec = codec; + return self; +} + +- (void)setStreamHandlerFactory:(NSObject *(^)(id))factory { + if (!factory) { + [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:nil]; + return; + } + + _streams = [NSMutableDictionary new]; + _listenerArguments = [NSMutableDictionary new]; + FlutterBinaryMessageHandler messageHandler = ^(NSData* message, FlutterBinaryReply callback) { + FlutterMethodCall* call = [self->_codec decodeMethodCall:message]; + NSArray *methodParts = [call.method componentsSeparatedByString:@"#"]; + + if (methodParts.count != 2) { + callback(nil); + return; + } + + NSInteger keyValue = [methodParts.lastObject integerValue]; + if(keyValue == 0) { + callback([self->_codec encodeErrorEnvelope:[FlutterError errorWithCode:@"error" message:[NSString stringWithFormat:@"Invalid method name: %@", call.method] details:nil]]); + return; + } + + NSNumber *key = [NSNumber numberWithInteger:keyValue]; + + if ([methodParts.firstObject isEqualToString:@"listen"]) { + [self listenForCall:call withKey:key usingCallback:callback andFactory:factory]; + } else if ([methodParts.firstObject isEqualToString:@"cancel"]) { + [self cancelForCall:call withKey:key usingCallback:callback andFactory:factory]; + } else { + callback(nil); + } + }; + + [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:messageHandler]; +} + +- (void) reset{ + for (NSString* key in self->_streams) { + AblyStreamsChannelStream *stream = self->_streams[key]; + [stream.handler onCancelWithArguments:nil]; + [_streams removeObjectForKey:key]; + [_listenerArguments removeObjectForKey:key]; + } +} + +- (void)listenForCall:(FlutterMethodCall*)call withKey:(NSNumber*)key usingCallback:(FlutterBinaryReply)callback andFactory:(NSObject *(^)(id))factory { + AblyStreamsChannelStream *stream = [AblyStreamsChannelStream new]; + stream.sink = ^(id event) { + NSString *name = [NSString stringWithFormat:@"%@#%@", self->_name, key]; + + if (event == FlutterEndOfEventStream) { + [self->_messenger sendOnChannel:name message:nil]; + } else if ([event isKindOfClass:[FlutterError class]]) { + [self->_messenger sendOnChannel:name + message:[self->_codec encodeErrorEnvelope:(FlutterError*)event]]; + } else { + [self->_messenger sendOnChannel:name message:[self->_codec encodeSuccessEnvelope:event]]; + } + }; + stream.handler = factory(call.arguments); + + [_streams setObject:stream forKey:key]; + [_listenerArguments setObject:call.arguments forKey:key]; + + FlutterError* error = [stream.handler onListenWithArguments:call.arguments eventSink:stream.sink]; + if (error) { + callback([self->_codec encodeErrorEnvelope:error]); + } else { + callback([self->_codec encodeSuccessEnvelope:nil]); + } +} + +- (void)cancelForCall:(FlutterMethodCall*)call withKey:(NSNumber*)key usingCallback:(FlutterBinaryReply)callback andFactory:(NSObject *(^)(id))factory { + AblyStreamsChannelStream *stream = [_streams objectForKey:key]; + if(!stream) { + callback([self->_codec encodeErrorEnvelope:[FlutterError errorWithCode:@"error" message:@"No active stream to cancel" details:nil]]); + return; + } + + [_streams removeObjectForKey:key]; + [_listenerArguments removeObjectForKey:key]; + + FlutterError* error = [stream.handler onCancelWithArguments:call.arguments]; + if (error) { + callback([self->_codec encodeErrorEnvelope:error]); + } else { + callback([self->_codec encodeSuccessEnvelope:nil]); + } +} + +@end diff --git a/ios/Classes/codec/AblyFlutterReader.m b/ios/Classes/codec/AblyFlutterReader.m index 7f43fc7e9..60b301f9f 100644 --- a/ios/Classes/codec/AblyFlutterReader.m +++ b/ios/Classes/codec/AblyFlutterReader.m @@ -50,7 +50,7 @@ -(id)readValueOfType:(const UInt8)type { if(decoder){ message = decoder(message); } - return [[AblyFlutterMessage alloc] initWithHandle:[dictionary objectForKey:TxAblyMessage_registrationHandle] message:message]; + return [[AblyFlutterMessage alloc] initWithMessage:message handle: [dictionary objectForKey:TxAblyMessage_registrationHandle]]; }; /** From b1375a97e9fa77fd07e2d6ecc089bcb6cf488fe3 Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Sun, 12 Jul 2020 23:22:25 +0530 Subject: [PATCH 09/15] removing streams channel dependency completely & example update 1. Update example code to suit well with new API 2. respective lock files updated after removing streams_channel --- example/ios/Podfile.lock | 6 --- example/lib/main.dart | 13 +++--- example/pubspec.lock | 7 ---- lib/src/impl/platform_object.dart | 3 +- lib/src/impl/streams_channel.dart | 70 +++++++++++++++++++++++++++++++ lib/src/platform.dart | 2 +- pubspec.lock | 7 ---- pubspec.yaml | 1 - 8 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 lib/src/impl/streams_channel.dart diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 284e27e98..f5785068c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -75,14 +75,11 @@ PODS: - msgpack (0.3.1) - SAMKeychain (1.5.3) - SocketRocketAblyFork (0.5.2-ably-4) - - streams_channel (0.0.1): - - Flutter - ULID (1.1.0) DEPENDENCIES: - ably_flutter_plugin (from `.symlinks/plugins/ably_flutter_plugin/ios`) - Flutter (from `Flutter`) - - streams_channel (from `.symlinks/plugins/streams_channel/ios`) SPEC REPOS: trunk: @@ -98,8 +95,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/ably_flutter_plugin/ios" Flutter: :path: Flutter - streams_channel: - :path: ".symlinks/plugins/streams_channel/ios" SPEC CHECKSUMS: Ably: b8dd747b116787f0759844dc1a045046ef9a2c7a @@ -109,7 +104,6 @@ SPEC CHECKSUMS: msgpack: a14de9216d29cfd0a7aff5af5150601a27e899a4 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SocketRocketAblyFork: ed717517ed5cb5217878987c84bc415890582bb3 - streams_channel: 16855ffd0568eded9563b2fec6a1bfa94420b353 ULID: b4714891a02819364faecd574a53e391c4c6de9d PODFILE CHECKSUM: 49ec7d4076524b7e225c38b98147173651ac4b9d diff --git a/example/lib/main.dart b/example/lib/main.dart index 9e1866238..a7a6a00e3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -27,8 +27,8 @@ class _MyAppState extends State { provisioning.AppKey _appKey; OpState _realtimeCreationState = OpState.NotStarted; OpState _restCreationState = OpState.NotStarted; - ably.RealtimeInterface _realtime; - ably.RestInterface _rest; + ably.Realtime _realtime; + ably.Rest _rest; ably.ConnectionState _realtimeConnectionState; @override @@ -97,7 +97,7 @@ class _MyAppState extends State { print("Custom logger :: $msg $exception"); }; - ably.RestInterface rest; + ably.Rest rest; try{ rest = ably.Rest(options: clientOptions); } catch (error) { @@ -123,6 +123,10 @@ class _MyAppState extends State { print(e.errorInfo); } print('Message published'); + + // set state here as handle will have been acquired before calling + // publish on channels above... + setState(() {}); } void createAblyRealtime() async { @@ -136,14 +140,13 @@ class _MyAppState extends State { }; clientOptions.autoConnect = false; - ably.RealtimeInterface realtime; + ably.Realtime realtime; try { realtime = ably.Realtime(options: clientOptions); //One can listen from multiple listeners on the same event, // and must cancel each subscription one by one //RETAINING LISTENER - α - print("starting α listener"); realtime.connection.on().listen((ably.ConnectionStateChange stateChange) async { print('RETAINING LISTENER α :: Change event arrived!: ${stateChange.event}'); setState(() { _realtimeConnectionState = stateChange.current; }); diff --git a/example/pubspec.lock b/example/pubspec.lock index 7182555fa..c545c5b01 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -170,13 +170,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" - streams_channel: - dependency: transitive - description: - name: streams_channel - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" string_scanner: dependency: transitive description: diff --git a/lib/src/impl/platform_object.dart b/lib/src/impl/platform_object.dart index cf55bcc08..09be560ac 100644 --- a/lib/src/impl/platform_object.dart +++ b/lib/src/impl/platform_object.dart @@ -3,7 +3,8 @@ import 'dart:async'; import 'package:ably_flutter_plugin/src/impl/message.dart'; import 'package:ably_flutter_plugin/src/platform.dart' as platform; import 'package:flutter/services.dart'; -import 'package:streams_channel/streams_channel.dart'; + +import 'streams_channel.dart'; /// An object which has a live counterpart in the Platform client library SDK, diff --git a/lib/src/impl/streams_channel.dart b/lib/src/impl/streams_channel.dart new file mode 100644 index 000000000..1c9267078 --- /dev/null +++ b/lib/src/impl/streams_channel.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +class StreamsChannel { + StreamsChannel(this.name, [this.codec = const StandardMethodCodec()]); + + /// The logical channel on which communication happens, not null. + final String name; + + /// The message codec used by this channel, not null. + final MethodCodec codec; + + int _lastId = 0; + + Stream receiveBroadcastStream([dynamic arguments]) { + final MethodChannel methodChannel = MethodChannel(name, codec); + + final id = ++_lastId; + final handlerName = '$name#$id'; + + StreamController controller; + controller = StreamController.broadcast(onListen: () async { + ServicesBinding.instance.defaultBinaryMessenger + .setMessageHandler(handlerName, (ByteData reply) async { + if (reply == null) { + await controller.close(); + } else { + try { + controller.add(codec.decodeEnvelope(reply)); + } on PlatformException catch (e) { + controller.addError(e); + } + } + + return reply; + }); + try { + await methodChannel.invokeMethod('listen#$id', arguments); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'streams_channel', + context: DiagnosticsNode.message( + 'while activating platform stream on channel $name'), + )); + } + }, onCancel: () async { + ServicesBinding.instance.defaultBinaryMessenger + .setMessageHandler(handlerName, null); + try { + await methodChannel.invokeMethod('cancel#$id', arguments); + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'streams_channel', + context: DiagnosticsNode.message( + 'while de-activating platform stream on channel $name'), + )); + } + }); + return controller.stream; + } +} diff --git a/lib/src/platform.dart b/lib/src/platform.dart index be883e64b..7caa81d6a 100644 --- a/lib/src/platform.dart +++ b/lib/src/platform.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:ably_flutter_plugin/src/generated/platformconstants.dart' show PlatformMethod; import 'package:flutter/services.dart'; -import 'package:streams_channel/streams_channel.dart'; import 'codec.dart'; +import 'impl/streams_channel.dart'; /// instance of [StandardMethodCodec] with custom [MessageCodec] for diff --git a/pubspec.lock b/pubspec.lock index 6d9a0eeb3..71b3cefb8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -331,13 +331,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" - streams_channel: - dependency: "direct main" - description: - name: streams_channel - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" string_scanner: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b32043378..1a9698d33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,6 @@ environment: dependencies: flutter: sdk: flutter - streams_channel: ^0.3.0 dev_dependencies: flutter_test: From c66d1a600fe50a293b300c9a74f120af2d4e4771 Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Sun, 12 Jul 2020 23:33:58 +0530 Subject: [PATCH 10/15] fix an issue with dart analysis --- lib/src/impl/realtime/channels.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/impl/realtime/channels.dart b/lib/src/impl/realtime/channels.dart index 4f7587e90..b28d8d4bc 100644 --- a/lib/src/impl/realtime/channels.dart +++ b/lib/src/impl/realtime/channels.dart @@ -85,12 +85,6 @@ class RealtimePlatformChannel extends PlatformObject implements spec.RealtimeCha return null; } - @override - Future off() { - // TODO: implement off - return null; - } - @override void setOptions(spec.ChannelOptions options) { // TODO: implement setOptions From ffdeb3e9a382e17d8f33613afd40c5953bb55bcc Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Sun, 12 Jul 2020 23:43:04 +0530 Subject: [PATCH 11/15] update DeveloperNotes.md with code analysis instructions --- DeveloperNotes.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/DeveloperNotes.md b/DeveloperNotes.md index 17dcfe5b5..3ceabf25b 100644 --- a/DeveloperNotes.md +++ b/DeveloperNotes.md @@ -57,3 +57,22 @@ Some files in the project are generated to maintain sync between Generated file paths are configured as values in `bin/codegen.dart` for `toGenerate` Map [Read about generation of platform specific constant files](bin/README.md) + + +## Static plugin code analyzer + +The new flutter analyzer does a great job at analyzing complete flutter package. + +Running `flutter analyze` in project root will analyze dart files in complete project, + i.e., plugin code and example code + + +Or, use the good old dart analyzer + +```bash +cd /lib/ +dartanalyzer --fatal-warnings **/*.dart + +cd /example/lib/ +dartanalyzer --fatal-warnings **/*.dart +``` From 1e3ecb83efd17db69dea8e267a490af4050d8eb5 Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Sun, 19 Jul 2020 00:56:22 +0530 Subject: [PATCH 12/15] minor fix in StreamsChannel, removed unwanted import --- android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java | 1 - 1 file changed, 1 deletion(-) diff --git a/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java b/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java index 04109233a..578fab1d1 100644 --- a/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java +++ b/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java @@ -15,7 +15,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import app.loup.streams_channel.BuildConfig; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import io.flutter.plugin.common.BinaryMessenger.BinaryReply; From 86deac257bdc6054137ddedbfe31e85cd716412c Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Mon, 20 Jul 2020 20:06:32 +0530 Subject: [PATCH 13/15] Addressing changes requested in iOS code review https://github.com/ably/ably-flutter/pull/18#pullrequestreview-451412366 1. rename singleton returning static method to `sharedInstance` 2. remove `dispose` from header to make it private 3. code indents/formatting 4. remove `self->` from `AblyStreamsChannel` wherever not required 5. improve DeveloperNotes.md --- DeveloperNotes.md | 6 ++---- ios/Classes/AblyFlutter.h | 3 +-- ios/Classes/AblyFlutter.m | 2 +- ios/Classes/AblyFlutterMessage.h | 2 +- ios/Classes/AblyFlutterPlugin.m | 7 +++++-- ios/Classes/AblyFlutterStreamHandler.m | 2 +- ios/Classes/AblyStreamsChannel.m | 14 +++++++------- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/DeveloperNotes.md b/DeveloperNotes.md index 3ceabf25b..d71e79437 100644 --- a/DeveloperNotes.md +++ b/DeveloperNotes.md @@ -70,9 +70,7 @@ Running `flutter analyze` in project root will analyze dart files in complete pr Or, use the good old dart analyzer ```bash -cd /lib/ -dartanalyzer --fatal-warnings **/*.dart +dartanalyzer --fatal-warnings lib/**/*.dart -cd /example/lib/ -dartanalyzer --fatal-warnings **/*.dart +dartanalyzer --fatal-warnings example/lib/**/*.dart ``` diff --git a/ios/Classes/AblyFlutter.h b/ios/Classes/AblyFlutter.h index 1d9647433..9e049fc20 100644 --- a/ios/Classes/AblyFlutter.h +++ b/ios/Classes/AblyFlutter.h @@ -8,7 +8,7 @@ NS_ASSUME_NONNULL_BEGIN @interface AblyFlutter : NSObject -+ (id)instance; ++ (instancetype)sharedInstance; -(NSNumber *)createRestWithOptions:(ARTClientOptions *)options; @@ -26,7 +26,6 @@ NS_ASSUME_NONNULL_BEGIN platform objects have been closed down cleanly. */ -(void)disposeWithCompletionHandler:(dispatch_block_t)completionHandler; --(void)dispose; @end diff --git a/ios/Classes/AblyFlutter.m b/ios/Classes/AblyFlutter.m index 735bfdc68..003b3a738 100644 --- a/ios/Classes/AblyFlutter.m +++ b/ios/Classes/AblyFlutter.m @@ -11,7 +11,7 @@ @implementation AblyFlutter { long long _nextHandle; } -+ (id)instance { ++ (instancetype)sharedInstance { static AblyFlutter *sharedInstance = nil; @synchronized(self) { if (sharedInstance == nil) { diff --git a/ios/Classes/AblyFlutterMessage.h b/ios/Classes/AblyFlutterMessage.h index 496e85b54..4e43bf555 100644 --- a/ios/Classes/AblyFlutterMessage.h +++ b/ios/Classes/AblyFlutterMessage.h @@ -8,7 +8,7 @@ NS_ASSUME_NONNULL_BEGIN -(instancetype)init NS_UNAVAILABLE; -(instancetype)initWithMessage:(id)message - handle:(NSNumber *)handle NS_DESIGNATED_INITIALIZER; + handle:(NSNumber *)handle NS_DESIGNATED_INITIALIZER; @property(nonatomic, readonly) NSNumber * handle; @property(nonatomic, readonly) id message; diff --git a/ios/Classes/AblyFlutterPlugin.m b/ios/Classes/AblyFlutterPlugin.m index f01244674..eb021f27c 100644 --- a/ios/Classes/AblyFlutterPlugin.m +++ b/ios/Classes/AblyFlutterPlugin.m @@ -110,7 +110,10 @@ +(void)registerWithRegistrar:(NSObject*)registrar { FlutterStandardMethodCodec *const methodCodec = [FlutterStandardMethodCodec codecWithReaderWriter:readerWriter]; // initializing event channel for event listeners - AblyStreamsChannel *const streamsChannel = [AblyStreamsChannel streamsChannelWithName:@"io.ably.flutter.stream" binaryMessenger:registrar.messenger codec: methodCodec]; + AblyStreamsChannel *const streamsChannel = + [AblyStreamsChannel streamsChannelWithName:@"io.ably.flutter.stream" + binaryMessenger:registrar.messenger + codec:methodCodec]; // initializing method channel for round-trip method calls FlutterMethodChannel *const channel = [FlutterMethodChannel methodChannelWithName:@"io.ably.flutter.plugin" binaryMessenger:[registrar messenger] codec:methodCodec]; @@ -131,7 +134,7 @@ -(instancetype)initWithChannel:(FlutterMethodChannel *const)channel streamsChann if (!self) { return nil; } - _ably = [AblyFlutter instance]; + _ably = [AblyFlutter sharedInstance]; self->_streamsChannel = streamsChannel; _handlers = @{ diff --git a/ios/Classes/AblyFlutterStreamHandler.m b/ios/Classes/AblyFlutterStreamHandler.m index 0a3550d54..66033d307 100644 --- a/ios/Classes/AblyFlutterStreamHandler.m +++ b/ios/Classes/AblyFlutterStreamHandler.m @@ -11,7 +11,7 @@ @implementation AblyFlutterStreamHandler{ @synthesize ably = _ably; - (instancetype)init{ - _ably = [AblyFlutter instance]; + _ably = [AblyFlutter sharedInstance]; listener = [ARTEventListener new]; return self; } diff --git a/ios/Classes/AblyStreamsChannel.m b/ios/Classes/AblyStreamsChannel.m index a445d3b37..7cc2aa02a 100644 --- a/ios/Classes/AblyStreamsChannel.m +++ b/ios/Classes/AblyStreamsChannel.m @@ -82,8 +82,8 @@ - (void)setStreamHandlerFactory:(NSObject *(^)(id))factory } - (void) reset{ - for (NSString* key in self->_streams) { - AblyStreamsChannelStream *stream = self->_streams[key]; + for (NSString* key in _streams) { + AblyStreamsChannelStream *stream = _streams[key]; [stream.handler onCancelWithArguments:nil]; [_streams removeObjectForKey:key]; [_listenerArguments removeObjectForKey:key]; @@ -111,16 +111,16 @@ - (void)listenForCall:(FlutterMethodCall*)call withKey:(NSNumber*)key usingCallb FlutterError* error = [stream.handler onListenWithArguments:call.arguments eventSink:stream.sink]; if (error) { - callback([self->_codec encodeErrorEnvelope:error]); + callback([_codec encodeErrorEnvelope:error]); } else { - callback([self->_codec encodeSuccessEnvelope:nil]); + callback([_codec encodeSuccessEnvelope:nil]); } } - (void)cancelForCall:(FlutterMethodCall*)call withKey:(NSNumber*)key usingCallback:(FlutterBinaryReply)callback andFactory:(NSObject *(^)(id))factory { AblyStreamsChannelStream *stream = [_streams objectForKey:key]; if(!stream) { - callback([self->_codec encodeErrorEnvelope:[FlutterError errorWithCode:@"error" message:@"No active stream to cancel" details:nil]]); + callback([_codec encodeErrorEnvelope:[FlutterError errorWithCode:@"error" message:@"No active stream to cancel" details:nil]]); return; } @@ -129,9 +129,9 @@ - (void)cancelForCall:(FlutterMethodCall*)call withKey:(NSNumber*)key usingCallb FlutterError* error = [stream.handler onCancelWithArguments:call.arguments]; if (error) { - callback([self->_codec encodeErrorEnvelope:error]); + callback([_codec encodeErrorEnvelope:error]); } else { - callback([self->_codec encodeSuccessEnvelope:nil]); + callback([_codec encodeSuccessEnvelope:nil]); } } From d99a8e40f166333522d84a33c7ee7c23c98714bc Mon Sep 17 00:00:00 2001 From: "Rohit R. Abbadi" Date: Mon, 20 Jul 2020 21:18:16 +0530 Subject: [PATCH 14/15] custom method `triggerCallback` to handle error/success response https://github.com/ably/ably-flutter/pull/18#discussion_r457211451 --- ios/Classes/AblyStreamsChannel.m | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/ios/Classes/AblyStreamsChannel.m b/ios/Classes/AblyStreamsChannel.m index 7cc2aa02a..ab47b936a 100644 --- a/ios/Classes/AblyStreamsChannel.m +++ b/ios/Classes/AblyStreamsChannel.m @@ -109,12 +109,8 @@ - (void)listenForCall:(FlutterMethodCall*)call withKey:(NSNumber*)key usingCallb [_streams setObject:stream forKey:key]; [_listenerArguments setObject:call.arguments forKey:key]; - FlutterError* error = [stream.handler onListenWithArguments:call.arguments eventSink:stream.sink]; - if (error) { - callback([_codec encodeErrorEnvelope:error]); - } else { - callback([_codec encodeSuccessEnvelope:nil]); - } + [self triggerCallback:callback + error:[stream.handler onListenWithArguments:call.arguments eventSink:stream.sink]]; } - (void)cancelForCall:(FlutterMethodCall*)call withKey:(NSNumber*)key usingCallback:(FlutterBinaryReply)callback andFactory:(NSObject *(^)(id))factory { @@ -127,12 +123,15 @@ - (void)cancelForCall:(FlutterMethodCall*)call withKey:(NSNumber*)key usingCallb [_streams removeObjectForKey:key]; [_listenerArguments removeObjectForKey:key]; - FlutterError* error = [stream.handler onCancelWithArguments:call.arguments]; - if (error) { - callback([_codec encodeErrorEnvelope:error]); - } else { - callback([_codec encodeSuccessEnvelope:nil]); - } + [self triggerCallback:callback error:[stream.handler onCancelWithArguments:call.arguments]]; +} + +- (void) triggerCallback:(FlutterBinaryReply)callback error:(FlutterError *const) error { + if (error) { + callback([_codec encodeErrorEnvelope:error]); + } else { + callback([_codec encodeSuccessEnvelope:nil]); + } } @end From 86df27e38e1dafead6485b55270e8237e8db8b14 Mon Sep 17 00:00:00 2001 From: Quintin Willison Date: Mon, 20 Jul 2020 17:41:45 +0100 Subject: [PATCH 15/15] Update copyright and license headers for source code derived from Loup sources. --- .../ably/flutter/plugin/StreamsChannel.java | 24 +++++++++++++++---- ios/Classes/AblyStreamsChannel.h | 21 ++++++++++++++-- ios/Classes/AblyStreamsChannel.m | 21 ++++++++++++++-- lib/src/impl/streams_channel.dart | 21 ++++++++++++++-- 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java b/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java index 578fab1d1..25f762d81 100644 --- a/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java +++ b/android/src/main/java/io/ably/flutter/plugin/StreamsChannel.java @@ -1,8 +1,22 @@ -// This file is derivative of work derived from original work at https://github.com/loup-v/streams_channel -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 -// Original file @ https://github.com/loup-v/streams_channel/blob/master/android/src/main/java/app/loup/streams_channel/StreamsChannel.java - +/* + * This file is derivative of work derived from original work at: + * https://github.com/loup-v/streams_channel + * + * Copyright 2018 Loup Inc. + * Copyright 2020 Ably Real-time Ltd (ably.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.ably.flutter.plugin; diff --git a/ios/Classes/AblyStreamsChannel.h b/ios/Classes/AblyStreamsChannel.h index 0d9aa72dd..a551e8f0a 100644 --- a/ios/Classes/AblyStreamsChannel.h +++ b/ios/Classes/AblyStreamsChannel.h @@ -1,5 +1,22 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 +/* + * This file is derivative of work derived from original work at: + * https://github.com/loup-v/streams_channel + * + * Copyright 2018 Loup Inc. + * Copyright 2020 Ably Real-time Ltd (ably.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #import diff --git a/ios/Classes/AblyStreamsChannel.m b/ios/Classes/AblyStreamsChannel.m index ab47b936a..db82ea366 100644 --- a/ios/Classes/AblyStreamsChannel.m +++ b/ios/Classes/AblyStreamsChannel.m @@ -1,5 +1,22 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 +/* + * This file is derivative of work derived from original work at: + * https://github.com/loup-v/streams_channel + * + * Copyright 2018 Loup Inc. + * Copyright 2020 Ably Real-time Ltd (ably.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #import "AblyStreamsChannel.h" diff --git a/lib/src/impl/streams_channel.dart b/lib/src/impl/streams_channel.dart index 1c9267078..deda094aa 100644 --- a/lib/src/impl/streams_channel.dart +++ b/lib/src/impl/streams_channel.dart @@ -1,5 +1,22 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 +/* + * This file is derivative of work derived from original work at: + * https://github.com/loup-v/streams_channel + * + * Copyright 2018 Loup Inc. + * Copyright 2020 Ably Real-time Ltd (ably.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import 'dart:async';