Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/transform-callback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tauri-apps/api": minor:changes
"tauri": minor:changes
---

`transformCallback` now registers the callbacks inside `window.__TAURI_INTERNALS__.callbacks` instead of directly on `window['_{id}']`
2 changes: 1 addition & 1 deletion crates/tauri/scripts/bundle.global.js

Large diffs are not rendered by default.

63 changes: 44 additions & 19 deletions crates/tauri/scripts/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,50 @@
}
})

Object.defineProperty(window.__TAURI_INTERNALS__, 'transformCallback', {
value: function transformCallback(callback, once) {
const identifier = uid()
const prop = `_${identifier}`
const callbacks = new Map()

Object.defineProperty(window, prop, {
value: (result) => {
if (once) {
Reflect.deleteProperty(window, prop)
}
function registerCallback(callback, once) {
const identifier = uid()
callbacks.set(identifier, (data) => {
if (once) {
unregisterCallback(identifier)
}
return callback && callback(data)
})
return identifier
}

return callback && callback(result)
},
writable: false,
configurable: true
})
function unregisterCallback(id) {
callbacks.delete(id)
}

return identifier
function runCallback(id, data) {
const callback = callbacks.get(id)
if (callback) {
callback(data)
} else {
console.warn(
`[TAURI] Couldn't find callback id ${id}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`
)
}
}

// Maybe let's rename it to `registerCallback`?
Object.defineProperty(window.__TAURI_INTERNALS__, 'transformCallback', {
value: registerCallback
})

Object.defineProperty(window.__TAURI_INTERNALS__, 'unregisterCallback', {
value: unregisterCallback
})

Object.defineProperty(window.__TAURI_INTERNALS__, 'runCallback', {
value: runCallback
})

// This is just for the debugging purposes
Object.defineProperty(window.__TAURI_INTERNALS__, 'callbacks', {
value: callbacks
})

const ipcQueue = []
Expand All @@ -56,13 +81,13 @@
Object.defineProperty(window.__TAURI_INTERNALS__, 'invoke', {
value: function (cmd, payload = {}, options) {
return new Promise(function (resolve, reject) {
const callback = window.__TAURI_INTERNALS__.transformCallback((r) => {
const callback = registerCallback((r) => {
resolve(r)
delete window[`_${error}`]
unregisterCallback(error)
}, true)
const error = window.__TAURI_INTERNALS__.transformCallback((e) => {
const error = registerCallback((e) => {
reject(e)
delete window[`_${callback}`]
unregisterCallback(callback)
}, true)

const action = () => {
Expand Down
20 changes: 7 additions & 13 deletions crates/tauri/scripts/ipc-protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,16 @@
headers
})
.then((response) => {
const cb =
const callbackId =
response.headers.get('Tauri-Response') === 'ok' ? callback : error
// we need to split here because on Android the content-type gets duplicated
switch ((response.headers.get('content-type') || '').split(',')[0]) {
case 'application/json':
return response.json().then((r) => [cb, r])
return response.json().then((r) => [callbackId, r])
case 'text/plain':
return response.text().then((r) => [cb, r])
return response.text().then((r) => [callbackId, r])
default:
return response.arrayBuffer().then((r) => [cb, r])
}
})
.then(([cb, data]) => {
if (window[`_${cb}`]) {
window[`_${cb}`](data)
} else {
console.warn(
`[TAURI] Couldn't find callback id {cb} in window. This might happen when the app is reloaded while Rust is running an asynchronous operation.`
)
return response.arrayBuffer().then((r) => [callbackId, r])
}
})
.catch((e) => {
Expand All @@ -71,6 +62,9 @@
customProtocolIpcFailed = true
sendIpcMessage(message)
})
.then(([callbackId, data]) => {
window.__TAURI_INTERNALS__.runCallback(callbackId, data)
})
} else {
// otherwise use the postMessage interface
const { data } = processIpcMessage({
Expand Down
17 changes: 10 additions & 7 deletions crates/tauri/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ mod event_name;

pub(crate) use event_name::EventName;

use crate::ipc::CallbackFn;

/// Unique id of an event.
pub type EventId = u32;

Expand Down Expand Up @@ -167,8 +169,9 @@ pub fn listen_js_script(
serialized_target: &str,
event: EventName<&str>,
event_id: EventId,
handler: &str,
handler: CallbackFn,
) -> String {
let handler_id = handler.0;
format!(
"(function () {{
if (window['{listeners_object_name}'] === void 0) {{
Expand All @@ -180,7 +183,7 @@ pub fn listen_js_script(
const eventListeners = window['{listeners_object_name}']['{event}']
const listener = {{
target: {serialized_target},
handler: {handler}
handlerId: {handler_id}
}};
Object.defineProperty(eventListeners, '{event_id}', {{ value: listener, configurable: true }});
}})()
Expand Down Expand Up @@ -211,23 +214,23 @@ pub fn unlisten_js_script(
"(function () {{
const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}']
if (listeners) {{
delete window['{listeners_object_name}']['{event_name}'][{event_id}];
window.__TAURI_INTERNALS__.unregisterCallback(listeners[{event_id}].handlerId)
}}
}})()
",
)
}

pub fn event_initialization_script(function: &str, listeners: &str) -> String {
pub fn event_initialization_script(function_name: &str, listeners: &str) -> String {
format!(
"Object.defineProperty(window, '{function}', {{
"Object.defineProperty(window, '{function_name}', {{
value: function (eventData, ids) {{
const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
for (const id of ids) {{
const listener = listeners[id]
if (listener && listener.handler) {{
if (listener) {{
eventData.id = id
listener.handler(eventData)
window.__TAURI_INTERNALS__.runCallback(listener.handlerId, eventData)
}}
}}
}}
Expand Down
31 changes: 18 additions & 13 deletions crates/tauri/src/ipc/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ use crate::{
};

use super::{
format_callback, CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response,
format_callback::format_raw_js, CallbackFn, InvokeError, InvokeResponseBody, IpcResponse,
Request, Response,
};

pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:";
Expand Down Expand Up @@ -150,15 +151,17 @@ impl JavaScriptChannelId {

match body {
// Don't go through the fetch process if the payload is small
InvokeResponseBody::Json(string) if string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD => {
webview.eval(format_callback::format_raw_js(
InvokeResponseBody::Json(json_string)
if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD =>
{
webview.eval(format_raw_js(
callback_id,
&format!("{{ message: {string}, index: {current_index} }}"),
format!("{{ message: {json_string}, index: {current_index} }}"),
))?;
}
InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => {
let bytes_as_json_array = serde_json::to_string(&bytes)?;
webview.eval(format_callback::format_raw_js(callback_id, &format!("{{ message: new Uint8Array({bytes_as_json_array}).buffer, index: {current_index} }}")))?;
webview.eval(format_raw_js(callback_id, format!("{{ message: new Uint8Array({bytes_as_json_array}).buffer, index: {current_index} }}")))?;
}
// use the fetch API to speed up larger response payloads
_ => {
Expand All @@ -172,7 +175,7 @@ impl JavaScriptChannelId {
.insert(data_id, body);

webview.eval(format!(
"window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window['_{callback_id}']({{ message: response, index: {current_index} }})).catch(console.error)",
"window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, {{ message: response, index: {current_index} }})).catch(console.error)",
))?;
}
}
Expand All @@ -181,9 +184,9 @@ impl JavaScriptChannelId {
}),
Some(Box::new(move || {
let current_index = counter_clone.load(Ordering::Relaxed);
let _ = webview_clone.eval(format_callback::format_raw_js(
let _ = webview_clone.eval(format_raw_js(
callback_id,
&format!("{{ end: true, index: {current_index} }}"),
format!("{{ end: true, index: {current_index} }}"),
));
})),
)
Expand Down Expand Up @@ -244,14 +247,16 @@ impl<TSend> Channel<TSend> {
Box::new(move |body| {
match body {
// Don't go through the fetch process if the payload is small
InvokeResponseBody::Json(string) if string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD => {
webview.eval(format_callback::format_raw_js(callback_id, &string))?;
InvokeResponseBody::Json(json_string)
if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD =>
{
webview.eval(format_raw_js(callback_id, json_string))?;
}
InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => {
let bytes_as_json_array = serde_json::to_string(&bytes)?;
webview.eval(format_callback::format_raw_js(
webview.eval(format_raw_js(
callback_id,
&format!("new Uint8Array({bytes_as_json_array}).buffer"),
format!("new Uint8Array({bytes_as_json_array}).buffer"),
))?;
}
// use the fetch API to speed up larger response payloads
Expand All @@ -266,7 +271,7 @@ impl<TSend> Channel<TSend> {
.insert(data_id, body);

webview.eval(format!(
"window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window['_{callback_id}'](response)).catch(console.error)",
"window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, response)).catch(console.error)",
))?;
}
}
Expand Down
39 changes: 22 additions & 17 deletions crates/tauri/src/ipc/format_callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,18 @@ pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result
/// than 10 KiB with `JSON.parse('...')`.
/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result<String> {
let callback_id = function_name.0;
serialize_js_with(json_string, Default::default(), |arg| {
format_raw_js(function_name.0, arg)
format_raw_js(callback_id, arg)
})
}

/// Formats a callback function invocation, properly accounting for error handling.
pub fn format_raw_js(id: u32, js: &str) -> String {
format!(
r#"
if (window["_{id}"]) {{
window["_{id}"]({js})
}} else {{
console.warn("[TAURI] Couldn't find callback id {id} in window. This happens when the app is reloaded while Rust is running an asynchronous operation.")
}}"#
)
/// Formats a function name and a JavaScript string argument to be evaluated as callback.
pub fn format_raw_js(callback_id: u32, js: impl AsRef<str>) -> String {
fn format_inner(callback_id: u32, js: &str) -> String {
format!("window.__TAURI_INTERNALS__.runCallback({callback_id}, {js})")
}
format_inner(callback_id, js.as_ref())
}

/// Formats a serializable Result type to its Promise response.
Expand Down Expand Up @@ -236,11 +233,11 @@ mod test {
// call format callback
let fc = format(f, &a).unwrap();
fc.contains(&format!(
r#"window["_{}"](JSON.parse('{}'))"#,
"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))",
f.0,
serde_json::Value::String(a.clone()),
)) || fc.contains(&format!(
r#"window["_{}"]({})"#,
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
f.0,
serde_json::Value::String(a),
))
Expand All @@ -256,7 +253,7 @@ mod test {
};

resp.contains(&format!(
r#"window["_{}"]({})"#,
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
function.0,
serde_json::Value::String(value),
))
Expand Down Expand Up @@ -320,8 +317,13 @@ mod test {
let a = a.0;
// call format callback
let fc = format_raw(f, a.clone()).unwrap();
fc.contains(&format!(r#"window["_{}"](JSON.parse('{}'))"#, f.0, a))
|| fc.contains(&format!(r#"window["_{}"]({})"#, f.0, a))
fc.contains(&format!(
r#"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))"#,
f.0, a
)) || fc.contains(&format!(
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
f.0, a
))
}

// check arbitrary strings in format_result
Expand All @@ -334,6 +336,9 @@ mod test {
Err(e) => (ec, e),
};

resp.contains(&format!(r#"window["_{}"]({})"#, function.0, value))
resp.contains(&format!(
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
function.0, value
))
}
}
2 changes: 1 addition & 1 deletion crates/tauri/src/webview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1668,7 +1668,7 @@ fn main() {
&serde_json::to_string(&target)?,
event,
id,
&format!("window['_{}']", handler.0),
handler,
))?;

listeners.listen_js(event, self.label(), target, id);
Expand Down
9 changes: 5 additions & 4 deletions packages/api/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,15 @@
export const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__'

/**
* Transforms a callback function to a string identifier that can be passed to the backend.
* Stores the callback in a known location, and returns an identifier that can be passed to the backend.
* The backend uses the identifier to `eval()` the callback.
*
* @return A unique identifier associated with the callback function.
* @return An unique identifier associated with the callback function.
*
* @since 1.0.0
*/
function transformCallback<T = unknown>(
// TODO: Make this not optional in v3
callback?: (response: T) => void,
once = false
): number {
Expand Down Expand Up @@ -131,7 +132,7 @@ class Channel<T = unknown> {
}

private cleanupCallback() {
Reflect.deleteProperty(window, `_${this.id}`)
window.__TAURI_INTERNALS__.unregisterCallback(this.id)
}

set onmessage(handler: (response: T) => void) {
Expand Down Expand Up @@ -325,7 +326,7 @@ export class Resource {
}

function isTauri(): boolean {
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
return !!((globalThis as any) || window).isTauri
}

Expand Down
Loading
Loading