Selaa lähdekoodia

feat(core): add IPC channel (#6813)

Lucas Fernandes Nogueira 2 vuotta sitten
vanhempi
sitoutus
0ab5f40d3a

+ 6 - 0
.changes/channel-api.md

@@ -0,0 +1,6 @@
+---
+"api": patch
+"tauri": patch
+---
+
+Add channel API for sending data across the IPC.

+ 11 - 0
core/tauri/mobile/android/src/main/java/app/tauri/plugin/Channel.kt

@@ -0,0 +1,11 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+package app.tauri.plugin
+
+class Channel(val id: Long, private val handler: (data: JSObject) -> Unit) {
+  fun send(data: JSObject) {
+    handler(data)
+  }
+}

+ 14 - 4
core/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt

@@ -6,19 +6,23 @@ package app.tauri.plugin
 
 import app.tauri.Logger
 
+const val CHANNEL_PREFIX = "__CHANNEL__:"
+
 class Invoke(
   val id: Long,
   val command: String,
-  private val sendResponse: (success: PluginResult?, error: PluginResult?) -> Unit,
+  val callback: Long,
+  val error: Long,
+  private val sendResponse: (callback: Long, data: PluginResult?) -> Unit,
   val data: JSObject) {
 
   fun resolve(data: JSObject?) {
     val result = PluginResult(data)
-    sendResponse(result, null)
+    sendResponse(callback, result)
   }
 
   fun resolve() {
-    sendResponse(null, null)
+    sendResponse(callback, null)
   }
 
   fun reject(msg: String?, code: String?, ex: Exception?, data: JSObject?) {
@@ -35,7 +39,7 @@ class Invoke(
     } catch (jsonEx: Exception) {
       Logger.error(Logger.tags("Plugin"), jsonEx.message!!, jsonEx)
     }
-    sendResponse(null, errorResult)
+    sendResponse(error, errorResult)
   }
 
   fun reject(msg: String?, ex: Exception?, data: JSObject?) {
@@ -197,4 +201,10 @@ class Invoke(
   fun hasOption(name: String): Boolean {
     return data.has(name)
   }
+
+  fun getChannel(name: String): Channel? {
+    val channelDef = getString(name, "")
+    val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: return null
+    return Channel(callback) { res -> sendResponse(callback, PluginResult(res)) }
+  }
 }

+ 3 - 3
core/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt

@@ -15,8 +15,8 @@ import app.tauri.Logger
 import app.tauri.PermissionHelper
 import app.tauri.PermissionState
 import app.tauri.annotation.ActivityCallback
-import app.tauri.annotation.PermissionCallback
 import app.tauri.annotation.Command
+import app.tauri.annotation.PermissionCallback
 import app.tauri.annotation.TauriPlugin
 import org.json.JSONException
 import java.util.*
@@ -126,7 +126,7 @@ abstract class Plugin(private val activity: Activity) {
 
       // If call was made without any custom permissions, request all from plugin annotation
       val aliasSet: MutableSet<String> = HashSet()
-      if (providedPermsList == null || providedPermsList.isEmpty()) {
+      if (providedPermsList.isNullOrEmpty()) {
         for (perm in annotation.permissions) {
           // If a permission is defined with no permission strings, separate it for auto-granting.
           // Otherwise, the alias is added to the list to be requested.
@@ -153,7 +153,7 @@ abstract class Plugin(private val activity: Activity) {
           permAliases = aliasSet.toTypedArray()
         }
       }
-      if (permAliases != null && permAliases.isNotEmpty()) {
+      if (!permAliases.isNullOrEmpty()) {
         // request permissions using provided aliases or all defined on the plugin
         requestPermissionForAliases(permAliases, invoke, "checkPermissions")
       } else if (autoGrantPerms.isNotEmpty()) {

+ 12 - 7
core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt

@@ -87,11 +87,7 @@ class PluginManager(val activity: AppCompatActivity) {
 
   @JniMethod
   fun postIpcMessage(webView: WebView, pluginId: String, command: String, data: JSObject, callback: Long, error: Long) {
-    val invoke = Invoke(callback, command, { successResult, errorResult ->
-      val (fn, result) = if (errorResult == null) Pair(callback, successResult) else Pair(
-        error,
-        errorResult
-      )
+    val invoke = Invoke(callback, command, callback, error, { fn, result ->
       webView.evaluateJavascript("window['_$fn']($result)", null)
     }, data)
 
@@ -100,8 +96,17 @@ class PluginManager(val activity: AppCompatActivity) {
 
   @JniMethod
   fun runCommand(id: Int, pluginId: String, command: String, data: JSObject) {
-    val invoke = Invoke(id.toLong(), command, { successResult, errorResult ->
-      handlePluginResponse(id, successResult?.toString(), errorResult?.toString())
+    val successId = 0L
+    val errorId = 1L
+    val invoke = Invoke(id.toLong(), command, successId, errorId, { fn, result ->
+      var success: PluginResult? = null
+      var error: PluginResult? = null
+      if (fn == successId) {
+        success = result
+      } else {
+        error = result
+      }
+      handlePluginResponse(id, success?.toString(), error?.toString())
     }, data)
 
     dispatchPluginMessage(invoke, pluginId)

+ 17 - 0
core/tauri/mobile/ios-api/Sources/Tauri/Channel.swift

@@ -0,0 +1,17 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+public class Channel {
+  var callback: UInt64
+  var handler: (JsonValue) -> Void
+
+  public init(callback: UInt64, handler: @escaping (JsonValue) -> Void) {
+    self.callback = callback
+    self.handler = handler
+  }
+
+  public func send(_ data: JsonObject) {
+    handler(.dictionary(data))
+  }
+}

+ 28 - 7
core/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift

@@ -5,6 +5,8 @@
 import Foundation
 import UIKit
 
+let CHANNEL_PREFIX = "__CHANNEL__:"
+
 @objc public class Invoke: NSObject, JSValueContainer, BridgedJSValueContainer {
 	public var dictionaryRepresentation: NSDictionary {
 		return data as NSDictionary
@@ -15,17 +17,21 @@ import UIKit
 	}()
 
   public var command: String
+  var callback: UInt64
+  var error: UInt64
 	public var data: JSObject
-	var sendResponse: (JsonValue?, JsonValue?) -> Void
+	var sendResponse: (UInt64, JsonValue?) -> Void
 
-	public init(command: String, sendResponse: @escaping (JsonValue?, JsonValue?) -> Void, data: JSObject?) {
+	public init(command: String, callback: UInt64, error: UInt64, sendResponse: @escaping (UInt64, JsonValue?) -> Void, data: JSObject?) {
     self.command = command
+    self.callback = callback
+    self.error = error
 		self.data = data ?? [:]
 		self.sendResponse = sendResponse
 	}
 
 	public func resolve() {
-		sendResponse(nil, nil)
+		sendResponse(callback, nil)
 	}
 
 	public func resolve(_ data: JsonObject) {
@@ -33,7 +39,7 @@ import UIKit
 	}
 
 	public func resolve(_ data: JsonValue) {
-		sendResponse(data, nil)
+		sendResponse(callback, data)
 	}
 
 	public func reject(_ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: JsonValue? = nil) {
@@ -46,7 +52,7 @@ import UIKit
 				}
 			}
 		}
-		sendResponse(nil, .dictionary(payload as! JsonObject))
+		sendResponse(self.error, .dictionary(payload as! JsonObject))
 	}
 
 	public func unimplemented() {
@@ -54,7 +60,7 @@ import UIKit
 	}
 
 	public func unimplemented(_ message: String) {
-		sendResponse(nil, .dictionary(["message": message]))
+		sendResponse(error, .dictionary(["message": message]))
 	}
 
 	public func unavailable() {
@@ -62,6 +68,21 @@ import UIKit
 	}
 
 	public func unavailable(_ message: String) {
-		sendResponse(nil, .dictionary(["message": message]))
+		sendResponse(error, .dictionary(["message": message]))
 	}
+
+  public func getChannel(_ key: String) -> Channel? {
+    let channelDef = getString(key, "")
+    if channelDef.starts(with: CHANNEL_PREFIX) {
+      let index = channelDef.index(channelDef.startIndex, offsetBy: CHANNEL_PREFIX.count)
+      guard let callback = UInt64(channelDef[index...]) else {
+        return nil
+      }
+      return Channel(callback: callback, handler: { (res: JsonValue) -> Void in
+        self.sendResponse(callback, res)
+      })
+    } else {
+      return nil
+    }
+  }
 }

+ 6 - 5
core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift

@@ -109,9 +109,8 @@ func onWebviewCreated(webview: WKWebView, viewController: UIViewController) {
 }
 
 @_cdecl("post_ipc_message")
-func postIpcMessage(webview: WKWebView, name: SRString, command: SRString, data: NSDictionary, callback: UInt, error: UInt) {
-	let invoke = Invoke(command: command.toString(), sendResponse: { (successResult: JsonValue?, errorResult: JsonValue?) -> Void in
-		let (fn, payload) = errorResult == nil ? (callback, successResult) : (error, errorResult)
+func postIpcMessage(webview: WKWebView, name: SRString, command: SRString, data: NSDictionary, callback: UInt64, error: UInt64) {
+	let invoke = Invoke(command: command.toString(), callback: callback, error: error, sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
 		var payloadJson: String
 		do {
 			try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
@@ -131,8 +130,10 @@ func runCommand(
 	data: NSDictionary,
 	callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>?) -> Void
 ) {
-	let invoke = Invoke(command: command.toString(), sendResponse: { (successResult: JsonValue?, errorResult: JsonValue?) -> Void in
-		let (success, payload) = errorResult == nil ? (true, successResult) : (false, errorResult)
+  let callbackId: UInt64 = 0
+  let errorId: UInt64 = 1
+	let invoke = Invoke(command: command.toString(), callback: callbackId, error: errorId, sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
+		let success = fn == callbackId
 		var payloadJson: String = ""
 		do {
 			try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
core/tauri/scripts/bundle.global.js


+ 2 - 0
core/tauri/scripts/stringify-ipc-message-fn.js

@@ -8,6 +8,8 @@
       let o = {};
       val.forEach((v, k) => o[k] = v);
       return o;
+    } else if (val instanceof Object && '__TAURI_CHANNEL_MARKER__' in val && typeof val.id === 'number') {
+      return `__CHANNEL__:${val.id}`
     } else {
       return val;
     }

+ 55 - 0
core/tauri/src/api/ipc.rs

@@ -10,6 +10,61 @@ use serde::{Deserialize, Serialize};
 use serde_json::value::RawValue;
 pub use serialize_to_javascript::Options as SerializeOptions;
 use serialize_to_javascript::Serialized;
+use tauri_macros::default_runtime;
+
+use crate::{
+  command::{CommandArg, CommandItem},
+  InvokeError, Runtime, Window,
+};
+
+const CHANNEL_PREFIX: &str = "__CHANNEL__:";
+
+/// An IPC channel.
+#[default_runtime(crate::Wry, wry)]
+pub struct Channel<R: Runtime> {
+  id: CallbackFn,
+  window: Window<R>,
+}
+
+impl Serialize for Channel {
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    serializer.serialize_str(&format!("{CHANNEL_PREFIX}{}", self.id.0))
+  }
+}
+
+impl<R: Runtime> Channel<R> {
+  /// Sends the given data through the channel.
+  pub fn send<S: Serialize>(&self, data: &S) -> crate::Result<()> {
+    let js = format_callback(self.id, data)?;
+    self.window.eval(&js)
+  }
+}
+
+impl<'de, R: Runtime> CommandArg<'de, R> for Channel<R> {
+  /// Grabs the [`Window`] from the [`CommandItem`] and returns the associated [`Channel`].
+  fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
+    let name = command.name;
+    let arg = command.key;
+    let window = command.message.window();
+    let value: String =
+      Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?;
+    if let Some(callback_id) = value
+      .split_once(CHANNEL_PREFIX)
+      .and_then(|(_prefix, id)| id.parse().ok())
+    {
+      return Ok(Channel {
+        id: CallbackFn(callback_id),
+        window,
+      });
+    }
+    Err(InvokeError::from_anyhow(anyhow::anyhow!(
+      "invalid channel value `{value}`, expected a string in the `{CHANNEL_PREFIX}ID` format"
+    )))
+  }
+}
 
 /// The `Callback` type is the return value of the `transformCallback` JavaScript function.
 #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
examples/api/dist/assets/index.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
examples/api/dist/assets/index.js


+ 0 - 12
examples/api/src/App.svelte

@@ -6,9 +6,7 @@
   import Cli from './views/Cli.svelte'
   import Communication from './views/Communication.svelte'
   import Window from './views/Window.svelte'
-  import Updater from './views/Updater.svelte'
   import WebRTC from './views/WebRTC.svelte'
-  import App from './views/App.svelte'
 
   import { onMount } from 'svelte'
   import { listen } from '@tauri-apps/api/event'
@@ -36,21 +34,11 @@
       component: Cli,
       icon: 'i-codicon-terminal'
     },
-    !isMobile && {
-      label: 'App',
-      component: App,
-      icon: 'i-codicon-hubot'
-    },
     !isMobile && {
       label: 'Window',
       component: Window,
       icon: 'i-codicon-window'
     },
-    !isMobile && {
-      label: 'Updater',
-      component: Updater,
-      icon: 'i-codicon-cloud-download'
-    },
     {
       label: 'WebRTC',
       component: WebRTC,

+ 0 - 76
examples/api/src/views/Updater.svelte

@@ -1,76 +0,0 @@
-<script>
-  import { onMount, onDestroy } from 'svelte'
-
-  // This example show how updater events work when dialog is disabled.
-  // This allow you to use custom dialog for the updater.
-  // This is your responsibility to restart the application after you receive the STATUS: DONE.
-
-  import { checkUpdate, installUpdate } from '@tauri-apps/api/updater'
-  import { listen } from '@tauri-apps/api/event'
-  import { relaunch } from '@tauri-apps/api/process'
-
-  export let onMessage
-  let unlisten
-
-  onMount(async () => {
-    unlisten = await listen('tauri://update-status', onMessage)
-  })
-  onDestroy(() => {
-    if (unlisten) {
-      unlisten()
-    }
-  })
-
-  let isChecking, isInstalling, newUpdate
-
-  async function check() {
-    isChecking = true
-    try {
-      const { shouldUpdate, manifest } = await checkUpdate()
-      onMessage(`Should update: ${shouldUpdate}`)
-      onMessage(manifest)
-
-      newUpdate = shouldUpdate
-    } catch (e) {
-      onMessage(e)
-    } finally {
-      isChecking = false
-    }
-  }
-
-  async function install() {
-    isInstalling = true
-    try {
-      await installUpdate()
-      onMessage('Installation complete, restart required.')
-      await relaunch()
-    } catch (e) {
-      onMessage(e)
-    } finally {
-      isInstalling = false
-    }
-  }
-</script>
-
-<div class="flex children:grow children:h10">
-  {#if !isChecking && !newUpdate}
-    <button class="btn" on:click={check}>Check update</button>
-  {:else if !isInstalling && newUpdate}
-    <button class="btn" on:click={install}>Install update</button>
-  {:else}
-    <button
-      class="btn text-accentText dark:text-darkAccentText flex items-center justify-center"
-      ><div class="spinner animate-spin" /></button
-    >
-  {/if}
-</div>
-
-<style>
-  .spinner {
-    height: 1.2rem;
-    width: 1.2rem;
-    border-radius: 50rem;
-    color: currentColor;
-    border: 2px dashed currentColor;
-  }
-</style>

+ 0 - 30
examples/api/src/views/Welcome.svelte

@@ -1,29 +1,3 @@
-<script>
-  import { relaunch, exit } from '@tauri-apps/api/process'
-
-  let version = '0.0.0'
-  let tauriVersion = '0.0.0'
-  let appName = 'Unknown'
-
-  getName().then((n) => {
-    appName = n
-  })
-  getVersion().then((v) => {
-    version = v
-  })
-  getTauriVersion().then((v) => {
-    tauriVersion = v
-  })
-
-  async function closeApp() {
-    await exit()
-  }
-
-  async function relaunchApp() {
-    await relaunch()
-  }
-</script>
-
 <p>
   This is a demo of Tauri's API capabilities using the <code
     >@tauri-apps/api</code
@@ -35,7 +9,3 @@
 <br />
 <br />
 <br />
-<div class="flex flex-wrap gap-1 shadow-">
-  <button class="btn" on:click={closeApp}>Close application</button>
-  <button class="btn" on:click={relaunchApp}>Relaunch application</button>
-</div>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
tooling/api/docs/js-api.json


+ 28 - 1
tooling/api/src/tauri.ts

@@ -55,6 +55,33 @@ function transformCallback(
   return identifier
 }
 
+class Channel<T = unknown> {
+  id: number
+  // @ts-expect-error field used by the IPC serializer
+  private readonly __TAURI_CHANNEL_MARKER__ = true
+  #onmessage: (response: T) => void = () => {
+    // no-op
+  }
+
+  constructor() {
+    this.id = transformCallback((response: T) => {
+      this.#onmessage(response)
+    })
+  }
+
+  set onmessage(handler: (response: T) => void) {
+    this.#onmessage = handler
+  }
+
+  get onmessage(): (response: T) => void {
+    return this.#onmessage
+  }
+
+  toJSON(): string {
+    return `__CHANNEL__:${this.id}`
+  }
+}
+
 /**
  * Command arguments.
  *
@@ -135,4 +162,4 @@ function convertFileSrc(filePath: string, protocol = 'asset'): string {
 
 export type { InvokeArgs }
 
-export { transformCallback, invoke, convertFileSrc }
+export { transformCallback, Channel, invoke, convertFileSrc }

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä