Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add IPC channel #6813

Merged
merged 25 commits into from May 11, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cfed95a
feat(core): add IPC channel
lucasfernog Apr 26, 2023
6737c68
simplify code
lucasfernog Apr 26, 2023
c4f7c14
add iOS impl
lucasfernog Apr 26, 2023
c578b17
impl serialize for channel
lucasfernog Apr 27, 2023
f6a0907
add change file
lucasfernog Apr 27, 2023
f5f3174
Merge remote-tracking branch 'origin/next' into feat/channel
lucasfernog Apr 28, 2023
2a19f01
revert callback/error cleanup removal
lucasfernog Apr 28, 2023
cffd9ce
Merge branch 'next' into feat/channel
lucasfernog May 1, 2023
c71f4fa
return class instead of string [skip ci]
lucasfernog May 1, 2023
1cf7f9a
cut string at prefix instead of using split
lucasfernog May 6, 2023
a40e9cd
add type
lucasfernog May 6, 2023
088e049
add headers
lucasfernog May 6, 2023
40a8422
Merge branch 'next' into feat/channel
lucasfernog May 6, 2023
d7c0552
fix: ios build
lucasfernog May 6, 2023
8a5240a
Merge branch 'next' into feat/channel
lucasfernog May 8, 2023
3ea4892
Merge remote-tracking branch 'origin/next' into feat/channel
lucasfernog May 9, 2023
36cb3bf
split_once
lucasfernog May 9, 2023
b435550
remove channel function, expose class
lucasfernog May 9, 2023
2ee8303
change channel API, add onmessage property
lucasfernog May 9, 2023
4d28f04
lint
lucasfernog May 9, 2023
6359fc9
fix ci
lucasfernog May 9, 2023
0da8170
Merge remote-tracking branch 'origin/next' into feat/channel
lucasfernog May 9, 2023
75e735d
fix impl on isolation pattern
lucasfernog May 10, 2023
823ac0a
fmt
lucasfernog May 10, 2023
9122e27
update example [skip ci]
lucasfernog May 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/channel-api.md
@@ -0,0 +1,6 @@
---
"api": patch
"tauri": patch
---

Add channel API for sending data across the IPC.
@@ -0,0 +1,7 @@
package app.tauri.plugin

class Channel(val id: Long, private val handler: (data: JSObject) -> Unit) {
fun send(data: JSObject) {
handler(data)
}
}
Expand Up @@ -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?) {
Expand All @@ -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?) {
Expand Down Expand Up @@ -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.split(CHANNEL_PREFIX)[1].toLongOrNull() ?: return null
return Channel(callback) { res -> sendResponse(callback, PluginResult(res)) }
}
}
Expand Up @@ -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.*
Expand Down Expand Up @@ -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.
Expand All @@ -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()) {
Expand Down
Expand Up @@ -85,11 +85,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)

Expand All @@ -98,8 +94,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)
Expand Down
13 changes: 13 additions & 0 deletions core/tauri/mobile/ios-api/Sources/Tauri/Channel.swift
@@ -0,0 +1,13 @@
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))
}
}
30 changes: 23 additions & 7 deletions core/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift
Expand Up @@ -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
Expand All @@ -15,25 +17,29 @@ 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) {
resolve(.dictionary(data))
}

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) {
Expand All @@ -46,22 +52,32 @@ import UIKit
}
}
}
sendResponse(nil, .dictionary(payload as! JsonObject))
sendResponse(self.error, .dictionary(payload as! JsonObject))
}

public func unimplemented() {
unimplemented("not implemented")
}

public func unimplemented(_ message: String) {
sendResponse(nil, .dictionary(["message": message]))
sendResponse(error, .dictionary(["message": message]))
}

public func unavailable() {
unavailable("not available")
}

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, "")
guard let callback = UInt64(channelDef.components(separatedBy: CHANNEL_PREFIX)[1]) else {
return nil
}
return Channel(callback: callback, handler: { (res: JsonValue) -> Void in
self.sendResponse(callback, res)
})
}
}
11 changes: 6 additions & 5 deletions core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift
Expand Up @@ -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`"
Expand All @@ -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`"
Expand Down
6 changes: 3 additions & 3 deletions core/tauri/scripts/bundle.global.js

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions core/tauri/scripts/core.js
Expand Up @@ -52,11 +52,9 @@
return new Promise(function (resolve, reject) {
var callback = window.__TAURI__.transformCallback(function (r) {
resolve(r)
delete window[`_${error}`]
}, true)
var error = window.__TAURI__.transformCallback(function (e) {
reject(e)
delete window[`_${callback}`]
}, true)

if (typeof cmd === 'string') {
Expand Down
58 changes: 58 additions & 0 deletions core/tauri/src/api/ipc.rs
Expand Up @@ -10,6 +10,64 @@ 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 value.starts_with(CHANNEL_PREFIX) {
let callback_id: Option<usize> = value
.split(CHANNEL_PREFIX)
.nth(1)
.and_then(|id| id.parse().ok());
if let Some(id) = callback_id {
return Ok(Channel {
id: CallbackFn(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)]
Expand Down
2 changes: 1 addition & 1 deletion tooling/api/docs/js-api.json

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions tooling/api/src/tauri.ts
Expand Up @@ -55,6 +55,17 @@ function transformCallback(
return identifier
}

/**
* Creates a channel using the given handler function.
*
* @returns the channel identifier to send to the IPC.
*
* @since 2.0.0
*/
function channel(fn: (response: any) => void): string {
return `__CHANNEL__:${transformCallback(fn)}`
}

/**
* Command arguments.
*
Expand All @@ -80,11 +91,9 @@ async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
return new Promise((resolve, reject) => {
const callback = transformCallback((e: T) => {
resolve(e)
Reflect.deleteProperty(window, `_${error}`)
}, true)
const error = transformCallback((e) => {
reject(e)
Reflect.deleteProperty(window, `_${callback}`)
}, true)

window.__TAURI_IPC__({
Expand Down Expand Up @@ -135,4 +144,4 @@ function convertFileSrc(filePath: string, protocol = 'asset'): string {

export type { InvokeArgs }

export { transformCallback, invoke, convertFileSrc }
export { transformCallback, channel, invoke, convertFileSrc }