mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-02-06 13:37:09 +00:00
refactor: rework transformCallback (#13325)
* refactor: rework `transformCallback` * Migrate listen and unlisten js * handlerId -> listener.handlerId * Update docs * `transformCallback` change file * typo
This commit is contained in:
parent
208f4bcadc
commit
b5c549d189
6
.changes/transform-callback.md
Normal file
6
.changes/transform-callback.md
Normal file
@ -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}']`
|
||||
File diff suppressed because one or more lines are too long
@ -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 = []
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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) => {
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {{
|
||||
@ -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 }});
|
||||
}})()
|
||||
@ -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)
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
@ -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__:";
|
||||
@ -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
|
||||
_ => {
|
||||
@ -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)",
|
||||
))?;
|
||||
}
|
||||
}
|
||||
@ -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} }}"),
|
||||
));
|
||||
})),
|
||||
)
|
||||
@ -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
|
||||
@ -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)",
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
@ -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),
|
||||
))
|
||||
@ -256,7 +253,7 @@ mod test {
|
||||
};
|
||||
|
||||
resp.contains(&format!(
|
||||
r#"window["_{}"]({})"#,
|
||||
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
|
||||
function.0,
|
||||
serde_json::Value::String(value),
|
||||
))
|
||||
@ -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
|
||||
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
@ -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) {
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
1
packages/api/src/global.d.ts
vendored
1
packages/api/src/global.d.ts
vendored
@ -12,6 +12,7 @@ declare global {
|
||||
__TAURI_INTERNALS__: {
|
||||
invoke: typeof invoke
|
||||
transformCallback: typeof transformCallback
|
||||
unregisterCallback: (number) => void
|
||||
convertFileSrc: typeof convertFileSrc
|
||||
ipc: (message: {
|
||||
cmd: string
|
||||
|
||||
@ -56,7 +56,7 @@ export async function newMenu(
|
||||
|
||||
if (opts && typeof opts === 'object') {
|
||||
if ('action' in opts && opts.action) {
|
||||
handler.onmessage = opts.action as () => void
|
||||
handler.onmessage = opts.action
|
||||
delete opts.action
|
||||
}
|
||||
|
||||
@ -107,7 +107,7 @@ export async function newMenu(
|
||||
|
||||
// @ts-expect-error the `prepareItem` return doesn't exactly match
|
||||
// this is fine, because the difference is in `[number, string]` variant
|
||||
opts.items = (opts.items as []).map(prepareItem)
|
||||
opts.items = opts.items.map(prepareItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ function mockInternals() {
|
||||
* # Examples
|
||||
*
|
||||
* Testing setup using Vitest:
|
||||
* ```js
|
||||
* ```ts
|
||||
* import { mockIPC, clearMocks } from "@tauri-apps/api/mocks"
|
||||
* import { invoke } from "@tauri-apps/api/core"
|
||||
*
|
||||
@ -66,36 +66,33 @@ export function mockIPC(
|
||||
): void {
|
||||
mockInternals()
|
||||
|
||||
window.__TAURI_INTERNALS__.transformCallback = function transformCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback?: (response: any) => void,
|
||||
const callbacks = new Map()
|
||||
|
||||
function registerCallback<T = unknown>(
|
||||
callback?: (response: T) => void,
|
||||
once = false
|
||||
) {
|
||||
const identifier = window.crypto.getRandomValues(new Uint32Array(1))[0]
|
||||
const prop = `_${identifier}`
|
||||
|
||||
Object.defineProperty(window, prop, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: (result: any) => {
|
||||
if (once) {
|
||||
Reflect.deleteProperty(window, prop)
|
||||
}
|
||||
|
||||
return callback && callback(result)
|
||||
},
|
||||
writable: false,
|
||||
configurable: true
|
||||
callbacks.set(identifier, (data: T) => {
|
||||
if (once) {
|
||||
unregisterCallback(identifier)
|
||||
}
|
||||
return callback && callback(data)
|
||||
})
|
||||
|
||||
return identifier
|
||||
}
|
||||
|
||||
function unregisterCallback(id: number) {
|
||||
callbacks.delete(id)
|
||||
}
|
||||
|
||||
window.__TAURI_INTERNALS__.transformCallback = registerCallback
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
window.__TAURI_INTERNALS__.invoke = async function (
|
||||
cmd: string,
|
||||
args?: InvokeArgs,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: InvokeOptions
|
||||
_options?: InvokeOptions
|
||||
): Promise<unknown> {
|
||||
return cb(cmd, args)
|
||||
} as typeof invoke
|
||||
|
||||
Loading…
Reference in New Issue
Block a user