diff --git a/PubNubUnity/Assets/PubNubUnity/Extensions/SdkExtensions.cs b/PubNubUnity/Assets/PubNubUnity/Extensions/SdkExtensions.cs
new file mode 100644
index 00000000..bc64a7af
--- /dev/null
+++ b/PubNubUnity/Assets/PubNubUnity/Extensions/SdkExtensions.cs
@@ -0,0 +1,16 @@
+using PubnubApi;
+using PubnubApi.EndPoint;
+using PubNubUnity.Internal;
+
+namespace PubNubUnity {
+ public static class SdkExtensions {
+ ///
+ /// Execute the publish operation and run the callback upon completion. The callback is dispatched to Unity main thread
+ ///
+ /// Publish operation
+ /// Callback to run upon operation completion
+ public static void Execute(this PublishOperation operation, System.Action callback) {
+ operation.Execute(new PNPublishResultExt((a, b) => PNDispatcher.Dispatch(() => callback?.Invoke(a, b))));
+ }
+ }
+}
diff --git a/PubNubUnity/Assets/PubNubUnity/Extensions/SdkExtensions.cs.meta b/PubNubUnity/Assets/PubNubUnity/Extensions/SdkExtensions.cs.meta
new file mode 100644
index 00000000..f983e312
--- /dev/null
+++ b/PubNubUnity/Assets/PubNubUnity/Extensions/SdkExtensions.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8544b57b7467fe94c97df53c5946d51a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/PubNubUnity/Assets/PubNubUnity/Utils/Editor.meta b/PubNubUnity/Assets/PubNubUnity/Utils/Editor.meta
new file mode 100644
index 00000000..6a58fb2d
--- /dev/null
+++ b/PubNubUnity/Assets/PubNubUnity/Utils/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: fad0b04c561290049aef3b9c9905eb49
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/PubNubUnity/Assets/PubNubUnity/Utils/Editor/PNDispatcherEditor.cs b/PubNubUnity/Assets/PubNubUnity/Utils/Editor/PNDispatcherEditor.cs
new file mode 100644
index 00000000..c60f1f71
--- /dev/null
+++ b/PubNubUnity/Assets/PubNubUnity/Utils/Editor/PNDispatcherEditor.cs
@@ -0,0 +1,12 @@
+using UnityEditor;
+
+namespace PubNubUnity.Internal {
+ [CustomEditor(typeof(PNDispatcher))]
+ public class PNDispatcherEditor : Editor {
+ public class DispatcherEditor : Editor {
+ public override void OnInspectorGUI() {
+ EditorGUILayout.HelpBox("This script allows dispatching to the main Unity render thread", MessageType.Info);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PubNubUnity/Assets/PubNubUnity/Utils/Editor/PNDispatcherEditor.cs.meta b/PubNubUnity/Assets/PubNubUnity/Utils/Editor/PNDispatcherEditor.cs.meta
new file mode 100644
index 00000000..1e65970b
--- /dev/null
+++ b/PubNubUnity/Assets/PubNubUnity/Utils/Editor/PNDispatcherEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f6184428bc11a3844be76626a046be94
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/PubNubUnity/Assets/PubNubUnity/Utils/PNDispatcher.cs b/PubNubUnity/Assets/PubNubUnity/Utils/PNDispatcher.cs
new file mode 100644
index 00000000..76c70380
--- /dev/null
+++ b/PubNubUnity/Assets/PubNubUnity/Utils/PNDispatcher.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using UnityEngine;
+
+namespace PubNubUnity.Internal {
+ public class PNDispatcher : MonoBehaviour {
+ static PNDispatcher instance;
+ static object lockObject;
+
+ static volatile Queue dispatchQueue = new Queue();
+
+ void FixedUpdate() {
+ HandleDispatch();
+ }
+
+ static void HandleDispatch() {
+ lock (lockObject) {
+ var c = dispatchQueue.Count;
+ for (int i = 0; i < c; i++) {
+ try {
+ dispatchQueue.Dequeue()();
+ } catch (System.Exception e) {
+ // TODO investigate if we need more error handling mechanisms
+ Debug.LogError($"{e.Message} ::\n{e.StackTrace}");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Dispatches the callback to Unity's main thread. Facilitates working on Unity's objects within the callback.
+ ///
+ /// Action to dispatch to main thread
+ public static void Dispatch(System.Action action) {
+ if (action is null) {
+ return;
+ }
+
+ lock (lockObject) {
+ dispatchQueue.Enqueue(action);
+ }
+ }
+
+ ///
+ /// Dispatch an async operation's result to the main thread. Facilitates working on Unity's objects within the callback.
+ ///
+ /// Async task to dispatch
+ /// Callback function which accepts task result as the argument
+ /// Task return type
+ public static async void DispatchTask(Task task, System.Action callback) {
+ if (callback is null) {
+ return;
+ }
+
+ T res;
+ if (task.IsCompleted) {
+ res = task.Result;
+ } else {
+ res = await task;
+ }
+
+ Dispatch(() => callback(res));
+ }
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
+ static void Initialize() {
+ if (!instance) {
+ instance = new GameObject("[PubNub Dispatcher]").AddComponent();
+ }
+ instance.gameObject.hideFlags = HideFlags.NotEditable | HideFlags.DontSave;
+ instance.transform.hideFlags = HideFlags.HideInInspector;
+
+ // For future potential edit-mode dispatching
+ if (Application.isPlaying) {
+ DontDestroyOnLoad(instance.gameObject);
+ }
+
+ lockObject ??= new object();
+ }
+ }
+}
diff --git a/PubNubUnity/Assets/PubNubUnity/Utils/PNDispatcher.cs.meta b/PubNubUnity/Assets/PubNubUnity/Utils/PNDispatcher.cs.meta
new file mode 100644
index 00000000..cb1daf34
--- /dev/null
+++ b/PubNubUnity/Assets/PubNubUnity/Utils/PNDispatcher.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a535dff998692134baec58cef1e5aa15
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: