Skip to content

Commit b5c549d

Browse files
refactor: rework transformCallback (#13325)
* refactor: rework `transformCallback` * Migrate listen and unlisten js * handlerId -> listener.handlerId * Update docs * `transformCallback` change file * typo
1 parent 208f4bc commit b5c549d

File tree

12 files changed

+134
-97
lines changed

12 files changed

+134
-97
lines changed

.changes/transform-callback.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tauri-apps/api": minor:changes
3+
"tauri": minor:changes
4+
---
5+
6+
`transformCallback` now registers the callbacks inside `window.__TAURI_INTERNALS__.callbacks` instead of directly on `window['_{id}']`

crates/tauri/scripts/bundle.global.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/tauri/scripts/core.js

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,50 @@
1919
}
2020
})
2121

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

27-
Object.defineProperty(window, prop, {
28-
value: (result) => {
29-
if (once) {
30-
Reflect.deleteProperty(window, prop)
31-
}
24+
function registerCallback(callback, once) {
25+
const identifier = uid()
26+
callbacks.set(identifier, (data) => {
27+
if (once) {
28+
unregisterCallback(identifier)
29+
}
30+
return callback && callback(data)
31+
})
32+
return identifier
33+
}
3234

33-
return callback && callback(result)
34-
},
35-
writable: false,
36-
configurable: true
37-
})
35+
function unregisterCallback(id) {
36+
callbacks.delete(id)
37+
}
3838

39-
return identifier
39+
function runCallback(id, data) {
40+
const callback = callbacks.get(id)
41+
if (callback) {
42+
callback(data)
43+
} else {
44+
console.warn(
45+
`[TAURI] Couldn't find callback id ${id}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`
46+
)
4047
}
48+
}
49+
50+
// Maybe let's rename it to `registerCallback`?
51+
Object.defineProperty(window.__TAURI_INTERNALS__, 'transformCallback', {
52+
value: registerCallback
53+
})
54+
55+
Object.defineProperty(window.__TAURI_INTERNALS__, 'unregisterCallback', {
56+
value: unregisterCallback
57+
})
58+
59+
Object.defineProperty(window.__TAURI_INTERNALS__, 'runCallback', {
60+
value: runCallback
61+
})
62+
63+
// This is just for the debugging purposes
64+
Object.defineProperty(window.__TAURI_INTERNALS__, 'callbacks', {
65+
value: callbacks
4166
})
4267

4368
const ipcQueue = []
@@ -56,13 +81,13 @@
5681
Object.defineProperty(window.__TAURI_INTERNALS__, 'invoke', {
5782
value: function (cmd, payload = {}, options) {
5883
return new Promise(function (resolve, reject) {
59-
const callback = window.__TAURI_INTERNALS__.transformCallback((r) => {
84+
const callback = registerCallback((r) => {
6085
resolve(r)
61-
delete window[`_${error}`]
86+
unregisterCallback(error)
6287
}, true)
63-
const error = window.__TAURI_INTERNALS__.transformCallback((e) => {
88+
const error = registerCallback((e) => {
6489
reject(e)
65-
delete window[`_${callback}`]
90+
unregisterCallback(callback)
6691
}, true)
6792

6893
const action = () => {

crates/tauri/scripts/ipc-protocol.js

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,16 @@
4040
headers
4141
})
4242
.then((response) => {
43-
const cb =
43+
const callbackId =
4444
response.headers.get('Tauri-Response') === 'ok' ? callback : error
4545
// we need to split here because on Android the content-type gets duplicated
4646
switch ((response.headers.get('content-type') || '').split(',')[0]) {
4747
case 'application/json':
48-
return response.json().then((r) => [cb, r])
48+
return response.json().then((r) => [callbackId, r])
4949
case 'text/plain':
50-
return response.text().then((r) => [cb, r])
50+
return response.text().then((r) => [callbackId, r])
5151
default:
52-
return response.arrayBuffer().then((r) => [cb, r])
53-
}
54-
})
55-
.then(([cb, data]) => {
56-
if (window[`_${cb}`]) {
57-
window[`_${cb}`](data)
58-
} else {
59-
console.warn(
60-
`[TAURI] Couldn't find callback id {cb} in window. This might happen when the app is reloaded while Rust is running an asynchronous operation.`
61-
)
52+
return response.arrayBuffer().then((r) => [callbackId, r])
6253
}
6354
})
6455
.catch((e) => {
@@ -71,6 +62,9 @@
7162
customProtocolIpcFailed = true
7263
sendIpcMessage(message)
7364
})
65+
.then(([callbackId, data]) => {
66+
window.__TAURI_INTERNALS__.runCallback(callbackId, data)
67+
})
7468
} else {
7569
// otherwise use the postMessage interface
7670
const { data } = processIpcMessage({

crates/tauri/src/event/mod.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ mod event_name;
1313

1414
pub(crate) use event_name::EventName;
1515

16+
use crate::ipc::CallbackFn;
17+
1618
/// Unique id of an event.
1719
pub type EventId = u32;
1820

@@ -167,8 +169,9 @@ pub fn listen_js_script(
167169
serialized_target: &str,
168170
event: EventName<&str>,
169171
event_id: EventId,
170-
handler: &str,
172+
handler: CallbackFn,
171173
) -> String {
174+
let handler_id = handler.0;
172175
format!(
173176
"(function () {{
174177
if (window['{listeners_object_name}'] === void 0) {{
@@ -180,7 +183,7 @@ pub fn listen_js_script(
180183
const eventListeners = window['{listeners_object_name}']['{event}']
181184
const listener = {{
182185
target: {serialized_target},
183-
handler: {handler}
186+
handlerId: {handler_id}
184187
}};
185188
Object.defineProperty(eventListeners, '{event_id}', {{ value: listener, configurable: true }});
186189
}})()
@@ -211,23 +214,23 @@ pub fn unlisten_js_script(
211214
"(function () {{
212215
const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}']
213216
if (listeners) {{
214-
delete window['{listeners_object_name}']['{event_name}'][{event_id}];
217+
window.__TAURI_INTERNALS__.unregisterCallback(listeners[{event_id}].handlerId)
215218
}}
216219
}})()
217220
",
218221
)
219222
}
220223

221-
pub fn event_initialization_script(function: &str, listeners: &str) -> String {
224+
pub fn event_initialization_script(function_name: &str, listeners: &str) -> String {
222225
format!(
223-
"Object.defineProperty(window, '{function}', {{
226+
"Object.defineProperty(window, '{function_name}', {{
224227
value: function (eventData, ids) {{
225228
const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
226229
for (const id of ids) {{
227230
const listener = listeners[id]
228-
if (listener && listener.handler) {{
231+
if (listener) {{
229232
eventData.id = id
230-
listener.handler(eventData)
233+
window.__TAURI_INTERNALS__.runCallback(listener.handlerId, eventData)
231234
}}
232235
}}
233236
}}

crates/tauri/src/ipc/channel.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ use crate::{
2121
};
2222

2323
use super::{
24-
format_callback, CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response,
24+
format_callback::format_raw_js, CallbackFn, InvokeError, InvokeResponseBody, IpcResponse,
25+
Request, Response,
2526
};
2627

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

151152
match body {
152153
// Don't go through the fetch process if the payload is small
153-
InvokeResponseBody::Json(string) if string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD => {
154-
webview.eval(format_callback::format_raw_js(
154+
InvokeResponseBody::Json(json_string)
155+
if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD =>
156+
{
157+
webview.eval(format_raw_js(
155158
callback_id,
156-
&format!("{{ message: {string}, index: {current_index} }}"),
159+
format!("{{ message: {json_string}, index: {current_index} }}"),
157160
))?;
158161
}
159162
InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => {
160163
let bytes_as_json_array = serde_json::to_string(&bytes)?;
161-
webview.eval(format_callback::format_raw_js(callback_id, &format!("{{ message: new Uint8Array({bytes_as_json_array}).buffer, index: {current_index} }}")))?;
164+
webview.eval(format_raw_js(callback_id, format!("{{ message: new Uint8Array({bytes_as_json_array}).buffer, index: {current_index} }}")))?;
162165
}
163166
// use the fetch API to speed up larger response payloads
164167
_ => {
@@ -172,7 +175,7 @@ impl JavaScriptChannelId {
172175
.insert(data_id, body);
173176

174177
webview.eval(format!(
175-
"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)",
178+
"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)",
176179
))?;
177180
}
178181
}
@@ -181,9 +184,9 @@ impl JavaScriptChannelId {
181184
}),
182185
Some(Box::new(move || {
183186
let current_index = counter_clone.load(Ordering::Relaxed);
184-
let _ = webview_clone.eval(format_callback::format_raw_js(
187+
let _ = webview_clone.eval(format_raw_js(
185188
callback_id,
186-
&format!("{{ end: true, index: {current_index} }}"),
189+
format!("{{ end: true, index: {current_index} }}"),
187190
));
188191
})),
189192
)
@@ -244,14 +247,16 @@ impl<TSend> Channel<TSend> {
244247
Box::new(move |body| {
245248
match body {
246249
// Don't go through the fetch process if the payload is small
247-
InvokeResponseBody::Json(string) if string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD => {
248-
webview.eval(format_callback::format_raw_js(callback_id, &string))?;
250+
InvokeResponseBody::Json(json_string)
251+
if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD =>
252+
{
253+
webview.eval(format_raw_js(callback_id, json_string))?;
249254
}
250255
InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => {
251256
let bytes_as_json_array = serde_json::to_string(&bytes)?;
252-
webview.eval(format_callback::format_raw_js(
257+
webview.eval(format_raw_js(
253258
callback_id,
254-
&format!("new Uint8Array({bytes_as_json_array}).buffer"),
259+
format!("new Uint8Array({bytes_as_json_array}).buffer"),
255260
))?;
256261
}
257262
// use the fetch API to speed up larger response payloads
@@ -266,7 +271,7 @@ impl<TSend> Channel<TSend> {
266271
.insert(data_id, body);
267272

268273
webview.eval(format!(
269-
"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)",
274+
"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)",
270275
))?;
271276
}
272277
}

crates/tauri/src/ipc/format_callback.rs

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,18 @@ pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result
9191
/// than 10 KiB with `JSON.parse('...')`.
9292
/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
9393
pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result<String> {
94+
let callback_id = function_name.0;
9495
serialize_js_with(json_string, Default::default(), |arg| {
95-
format_raw_js(function_name.0, arg)
96+
format_raw_js(callback_id, arg)
9697
})
9798
}
9899

99-
/// Formats a callback function invocation, properly accounting for error handling.
100-
pub fn format_raw_js(id: u32, js: &str) -> String {
101-
format!(
102-
r#"
103-
if (window["_{id}"]) {{
104-
window["_{id}"]({js})
105-
}} else {{
106-
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.")
107-
}}"#
108-
)
100+
/// Formats a function name and a JavaScript string argument to be evaluated as callback.
101+
pub fn format_raw_js(callback_id: u32, js: impl AsRef<str>) -> String {
102+
fn format_inner(callback_id: u32, js: &str) -> String {
103+
format!("window.__TAURI_INTERNALS__.runCallback({callback_id}, {js})")
104+
}
105+
format_inner(callback_id, js.as_ref())
109106
}
110107

111108
/// Formats a serializable Result type to its Promise response.
@@ -236,11 +233,11 @@ mod test {
236233
// call format callback
237234
let fc = format(f, &a).unwrap();
238235
fc.contains(&format!(
239-
r#"window["_{}"](JSON.parse('{}'))"#,
236+
"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))",
240237
f.0,
241238
serde_json::Value::String(a.clone()),
242239
)) || fc.contains(&format!(
243-
r#"window["_{}"]({})"#,
240+
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
244241
f.0,
245242
serde_json::Value::String(a),
246243
))
@@ -256,7 +253,7 @@ mod test {
256253
};
257254

258255
resp.contains(&format!(
259-
r#"window["_{}"]({})"#,
256+
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
260257
function.0,
261258
serde_json::Value::String(value),
262259
))
@@ -320,8 +317,13 @@ mod test {
320317
let a = a.0;
321318
// call format callback
322319
let fc = format_raw(f, a.clone()).unwrap();
323-
fc.contains(&format!(r#"window["_{}"](JSON.parse('{}'))"#, f.0, a))
324-
|| fc.contains(&format!(r#"window["_{}"]({})"#, f.0, a))
320+
fc.contains(&format!(
321+
r#"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))"#,
322+
f.0, a
323+
)) || fc.contains(&format!(
324+
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
325+
f.0, a
326+
))
325327
}
326328

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

337-
resp.contains(&format!(r#"window["_{}"]({})"#, function.0, value))
339+
resp.contains(&format!(
340+
r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#,
341+
function.0, value
342+
))
338343
}
339344
}

crates/tauri/src/webview/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1668,7 +1668,7 @@ fn main() {
16681668
&serde_json::to_string(&target)?,
16691669
event,
16701670
id,
1671-
&format!("window['_{}']", handler.0),
1671+
handler,
16721672
))?;
16731673

16741674
listeners.listen_js(event, self.label(), target, id);

packages/api/src/core.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,15 @@
5959
export const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__'
6060

6161
/**
62-
* Transforms a callback function to a string identifier that can be passed to the backend.
62+
* Stores the callback in a known location, and returns an identifier that can be passed to the backend.
6363
* The backend uses the identifier to `eval()` the callback.
6464
*
65-
* @return A unique identifier associated with the callback function.
65+
* @return An unique identifier associated with the callback function.
6666
*
6767
* @since 1.0.0
6868
*/
6969
function transformCallback<T = unknown>(
70+
// TODO: Make this not optional in v3
7071
callback?: (response: T) => void,
7172
once = false
7273
): number {
@@ -131,7 +132,7 @@ class Channel<T = unknown> {
131132
}
132133

133134
private cleanupCallback() {
134-
Reflect.deleteProperty(window, `_${this.id}`)
135+
window.__TAURI_INTERNALS__.unregisterCallback(this.id)
135136
}
136137

137138
set onmessage(handler: (response: T) => void) {
@@ -325,7 +326,7 @@ export class Resource {
325326
}
326327

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

0 commit comments

Comments
 (0)