Engine and windows
This page explains how the Tauri SDK works: why there's an engine window, every engine page option, how the engine starts and is rebuilt, multiple UI windows, who alerts the user, handling alerts and calls in the engine page, exiting and tray apps, single instance and multiple AppKeys, and where tokens and local data are stored.
| You want to | See |
|---|---|
| Write the engine page | Engine page, Engine page options |
| Use the SDK in UI windows, open several windows | UI windows, Multiple windows |
| Show "Reconnecting", handle engine crashes | Engine state |
| Decide who alerts, show notifications or handle calls yourself | Who alerts, Alerts and calls in the engine page |
| Minimize to the tray when windows close | Exiting and tray apps |
| Find where tokens and chat history are stored, encrypt the local database | Tokens and local data |
| See every Rust setting | Rust settings |
Why an engine window
Desktop IM apps usually keep running in the tray after the user closes the main window, and show a notification when a message arrives. If the connection lived in a UI window, closing the window would drop it. So the SDK puts the connection in an engine window:
- The plugin creates it after the app starts and loads the engine page you provide. It's hidden: it doesn't appear in the taskbar, Dock or window switcher, never takes focus, and lives as long as the process.
- The engine holds the connection and the sign-in tokens, and handles sync, writing local data, the send queue, token renewal, and system notifications and incoming calls when no window is visible.
- Your UI windows each create their own client and read local data directly. Operations that need the server (sending messages, editing profiles, uploading and so on) are passed to the engine by the SDK, and the results come back unchanged. Events are emitted after the engine writes the local data, and every window receives them.
This works like multiple tabs in the same browser in the Web SDK, except that the engine window always owns the connection and it never moves between windows. The cost is one extra WebView's memory, and one extra WebView created at startup.
Engine page
The engine page only calls createDRTauriEngine() to create the engine client, and registers alert and call handlers if needed:
// src/engine.ts
import { createDRTauriEngine } from '@deeprespond/im-tauri/engine';
const engine = createDRTauriEngine({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
notifications: { mode: 'auto' },
});
// 引擎客户端,可以像界面窗口中的客户端一样使用
engine.client.on('auth.stateChanged', (state) => console.log('登录状态', state.kind));- It must be a page bundled in the app. The plugin refuses remote addresses so remote content can't get the sign-in tokens. The engine page and the UI pages come from the same build, so the SDK version matches.
- It only does alert-related work. It shows no UI, imports no UI framework and doesn't touch your business data. It shares the same local data with the UI windows but not their memory.
- One engine per AppKey. Creating another for an AppKey that already has one rejects with
invalid_state(client_exists). CallingcreateDRTauriEngine()in a UI window throwsunsupported(not_engine_window); conversely, callingcreateDRTauriClient()in the engine page throwsunsupported(not_ui_window). - The engine window's label is always
deeprespond-im-engine, and the plugin grants its permissions automatically.
Engine page options
| Option | Type | Required | Description |
|---|---|---|---|
appKey | string | Yes | The app's AppKey, in the form org_name#app_name |
apiUrl | string | Yes | IM service address, such as https://im.example.com |
wsUrl | string | No | Connection address; derived from apiUrl by default, as in the Web SDK |
deviceName | string | No | Device name shown in the signed-in device list. Defaults to the computer name, up to 64 characters |
appVersion | string | No | Your app's version, reported on the connection to help troubleshooting |
ticketProvider | (ctx: TicketContext) => Promise<string> | No | Function that gets a new sign-in ticket, run in the engine page; see Getting tickets again automatically in the Web SDK |
encryptDatabase | boolean | No | Encrypt the local database; default false. Needs the plugin's sqlcipher feature; see Encrypt the local database |
autoMarkRead | boolean | No | Mark a conversation read automatically while the user is looking at it; default false. "Looking" requires the window to be visible and focused |
conversationWindow | number | No | Maximum conversations kept in memory for the conversation list; default 1000 |
userCacheTtlMs | number | No | How long other users' profiles are cached, in milliseconds; default 600000 |
outbox | { maxAgeHours?: number; persistAttachmentMaxBytes?: number } | No | Send queue: how long a message may wait to be sent (default 48 hours), and the size up to which attachments are copied and kept (default 200 MB; larger ones only have their path recorded). See Send local files |
media | { compressImages?: boolean | { maxSide?: number; quality?: number }; preprocess?: (input: Blob, kind: FileKind) => Promise<Blob> } | No | Processing before upload: image compression settings (default long side 2048, quality 0.85; false sends originals), or your own processing function; for the rules, see Processing before upload in the Web SDK. With preprocess set, local files given by path are read into memory first to be passed to it |
notifications.mode | 'auto' | 'manual' | No | Whether the SDK shows system notifications itself (auto, the default) or leaves it to you (manual); see System notifications |
notifications.sound | boolean | No | Whether automatic notifications play the system sound; default true |
notifications.notices | boolean | No | Whether to alert for friend requests, group invitations and join requests; default true |
notifications.calls | 'auto' | 'manual' | No | Whether calls arriving with no visible window are shown as a notification with a ringtone by the SDK (auto, the default) or passed to onIncomingCall (manual) |
notifications.ringtone | string | No | Ringtone file address the engine page can reach (such as /ringtone.mp3 in Vite's public directory), played on a loop. Defaults to a ringtone synthesized by the SDK |
notifications.format | (alert: EngineAlert) => { title: string; body: string } | null | No | Custom text for automatic notifications; returning null skips the alert. See Notification text |
badge | 'auto' | 'manual' | No | Whether the SDK shows the unread count on the Dock, taskbar or launcher (auto, the default) or you call desktop.setBadge(); see Badges |
badgeIncludesMuted | boolean | No | Whether the badge counts muted conversations; default false |
attention | boolean | No | Whether automatic alerts also ask the system for attention (flashing taskbar, bouncing Dock icon); default true |
logger | { level?: LogLevel; sink?: (entry: LogEntry) => void } | No | Log level and output function, as in the Web SDK's Logging |
There's no platform, storage, tokenStorage, persistStorage or sessionScope from the Web SDK: the plugin sets the platform per system and decides how data and tokens are stored.
UI windows
Each window creates its own client with createDRTauriClient():
import { createDRTauriClient } from '@deeprespond/im-tauri';
const im = createDRTauriClient({
appKey: '1575529652#demo',
logger: { level: 'info' }, // 可选:本窗口的日志
});
console.log(im.engineState, im.desktop.info.os);- Only the AppKey is needed. The engine decides the server address and other options.
- It's a Web SDK client as far as you're concerned. Every module's API,
LiveListand events are the same, and the React and Vue bindings work directly. It addsim.desktop(see Desktop features) andim.engineState. - One client per window. A window can have only one per AppKey; another rejects with
invalid_state(client_exists). The plugin releases it automatically when the window closes, so you don't calldestroy(). - Reads don't go through the engine. The plugin returns local data such as conversations and messages directly, so the last content can be shown even before the engine is ready.
- Calls that need the server all go to the engine. That includes standalone uploads and getting download addresses, which tabs do themselves in the Web SDK. While the engine isn't ready yet or is being rebuilt, calls wait in a queue for up to 10 seconds, then fail with
engine_unavailable(details.reasonisstarting,restartingorfailed). - Windows hold no tokens.
im.auth.currentUseronly has the username and session ID; the access and refresh tokens exist only in the engine. Even a window that loads a bad third-party script can't steal them.
Multiple windows
You can open several windows at once, for example to put a conversation in its own window. The new window's label must be granted in capabilities (see Grant window permissions), and the window opening it needs permission to create windows (core:webview:allow-create-webview-window):
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
function openInNewWindow(conversationKey: string) {
// 新窗口加载同一个页面,照常创建客户端,从地址中取出要打开的会话
new WebviewWindow(`chat-${Date.now()}`, {
url: `index.html?conversation=${encodeURIComponent(conversationKey)}`,
title: '会话',
width: 700,
height: 600,
});
}- Every window reads the same local data and receives the same events, and their lists update on their own. When one window signs in, signs out or switches users, the others follow.
- "Looking at" a conversation: its message list is open, at the latest message and on screen, and the window is visible and focused. The focus requirement is extra compared with the Web SDK: when the window is covered by another program, the user isn't looking. Minimized and hidden windows and a locked screen all count as not visible. Conversations being looked at aren't alerted, and are marked read automatically with
autoMarkRead. - When a window closes, its client releases its lists, unsubscribes from presence and leaves chat rooms it entered; calls in that window are treated as hung up. If the window crashes, the plugin tells the engine on its behalf, with the same effect.
Engine state
im.engineState in a UI window is the engine's current state, and engine.stateChanged is emitted when it changes:
| State | Description |
|---|---|
starting | The engine is starting, or the engine page hasn't created the engine client for this AppKey yet |
ready | The engine is ready |
restarting | The engine crashed or hung and the plugin is rebuilding it |
failed | The engine is unavailable: it crashed 3 times within 1 minute and rebuilding stopped; the engine page didn't create the engine client within 10 seconds of loading (wrong engine_url, a script error on the page); or another process is using the same data (see Single instance). Ask the user to restart the app |
im.on('engine.stateChanged', ({ state }) => {
if (state === 'restarting') showToast('正在重新连接…');
if (state === 'failed') showToast('IM 服务不可用,请重新启动应用');
});im.connection.state is still the connection state, as in the Web SDK. The engine being ready doesn't mean it's connected to the server.
Crashes and rebuilds
When the engine window's WebView process crashes, hangs (no response for 30 seconds), or the page reloads unexpectedly, the plugin rebuilds the engine window after 1 second. The rebuilt engine restores sign-in, reconnects and syncs like a normal start, with nothing for you to do:
- During the rebuild, calls in windows that need the server wait in the queue as above; reading local data isn't affected.
- Local writes that hadn't committed are rolled back, so no half-written data is left; messages in the send queue continue after the rebuild.
- Uploads in progress continue from the uploaded part in the new engine; downloads are done by the plugin and aren't affected.
- Hot reloading the engine page during development is also treated as a rebuild.
After 3 crashes within 1 minute, or if the engine page fails to load, the plugin stops rebuilding, the state becomes failed, and the reason goes to the log file (see Diagnostics and logs).
Who alerts
Every window and the engine receive events for new messages and incoming calls. To avoid alerting twice, the SDK picks one to do the alerting, by the same rules as the Web SDK's alerting tab, with "tab" replaced by "window":
- When there are visible UI windows, the most recently focused one is responsible.
- When there are no UI windows, or none are visible (minimized, in the tray), the engine is responsible.
When alert is true in the payload of message.received or call.incoming, this window is responsible for alerting. im.connection.isAlertTab says whether this window is responsible right now, and window.roleChanged is emitted when that changes (replacing the Web SDK's tabs.roleChanged, with the same payload; isLeader is always false in UI windows).
| Alert | When a visible window is responsible | When the engine is responsible (no visible window) |
|---|---|---|
New message (notifications.mode is auto) | If the window isn't focused, the SDK shows a system notification; if it is, none is shown and you can show something in the UI | The SDK shows a system notification |
New message (manual) | You call im.desktop.notify() in the window based on alert | Passed to the engine page's onAlert |
Incoming call (notifications.calls is auto) | You show the incoming call UI and play a ringtone on call.incoming in the window, as in the Web SDK | The SDK shows a notification with Answer and Decline and plays the ringtone in the engine page |
Incoming call (manual) | Same as above | Passed to the engine page's onIncomingCall |
In automatic mode, notifications are always shown by the engine, whichever window is responsible, so they're never duplicated. For notification content, clicks and permissions, see System notifications.
Alerts and calls in the engine page
With no visible window, the engine page is the only place in the app running JavaScript. To decide how to alert yourself, register handlers in the engine page. They're only called when the engine is responsible for alerting; when a window is visible, alerts are handled by code in that window.
Show notifications yourself
With notifications.mode set to manual, the SDK doesn't show notifications for new messages, friend requests and the like, and passes them to onAlert:
import { createDRTauriEngine } from '@deeprespond/im-tauri/engine';
const engine = createDRTauriEngine({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
notifications: { mode: 'manual' },
});
engine.onAlert(async (alert) => {
if (alert.kind === 'message') {
// notify 已按用户的推送设置和会话免打扰判断过,只有应该提醒的才会交来
const { message, notify } = alert;
await engine.client.desktop.notify({
id: `msg:${message.conversation_id}`, // 同一会话只保留一条
title: notify.title || '新消息',
body: notify.body ?? '',
conversation_id: message.conversation_id,
seq: message.seq ?? undefined,
});
} else {
// 好友申请、入群邀请、入群申请:标题和正文已经生成好
await engine.client.desktop.notify({ title: alert.title, body: alert.body });
}
});onAlert receives an EngineAlert of one of two kinds:
kind | Fields | Description |
|---|---|---|
message | message, notify | The new message and the alert decision (NotifyDecision) |
notice | type, title, body, user, group_id | type is friend_request (a friend request; user is the requester) or group_request (a group invitation or join request; group_id is the group) |
onAlert returns a function that unregisters the handler. Exceptions thrown by the handler are logged and don't affect the SDK.
Notification text
The SDK writes the text of automatic notifications, in Chinese only: the title is the sender or group name, and the body follows the user's preview setting (with previews off, just "你收到了一条新消息", "You received a new message"), by the same rules as the Web SDK's alert decision. For other languages or your own wording, give notifications.format:
import { createDRTauriEngine, type DREngine, type EngineAlert } from '@deeprespond/im-tauri/engine';
const text = { newMessage: 'New message', hidden: 'You have a new message', friendRequest: 'Friend request', groupRequest: 'Group request' };
const engine: DREngine = createDRTauriEngine({
appKey: '1575529652#demo',
apiUrl: 'https://im.example.com',
notifications: { format: (alert) => format(alert) },
});
function format(alert: EngineAlert): { title: string; body: string } | null {
if (alert.kind === 'notice') {
return { title: alert.type === 'friend_request' ? text.friendRequest : text.groupRequest, body: '' };
}
// 按用户的预览方式决定显示多少内容
const preview = engine.client.push.settings()?.effective_preview ?? 'full';
if (preview === 'none') return { title: '', body: text.hidden };
const title = alert.notify.title || text.newMessage; // 发送者或群名
if (preview === 'sender_only' || alert.message.type !== 'text') return { title, body: text.newMessage };
return { title, body: String(alert.message.body?.text ?? '') };
}If format throws, the default text is used. Notifications for being signed in on another device or the sign-in expiring (see Notifications when the session ends) and call notifications don't go through it.
Handle calls yourself
With notifications.calls set to manual, calls that arrive with no visible window go to onIncomingCall, which gets the Web SDK's call object CallHandle:
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
engine.onIncomingCall(async (call) => {
const snapshot = call.getSnapshot();
if (!engine.client.calls.supported) {
await engine.client.desktop.notify({ title: '来电', body: '请在手机上接听' });
return;
}
// 打开一个小窗口显示来电,在那个窗口中接听
new WebviewWindow(`call-${call.call_id}`, {
url: `index.html?call=${encodeURIComponent(call.call_id)}`,
title: snapshot.call.media === 'video' ? '视频来电' : '语音来电',
width: 360,
height: 480,
alwaysOnTop: true,
});
});Calls can't be answered in the engine. The engine window is hidden and doesn't run media connections. You can reject in the engine (
call.reject()); answering must happen in a UI window, whose client finds the call bycall_idinim.calls.incomingand callsjoin(). See Answer and reject in the Web SDK.By default the engine window only has the plugin's permissions. To create windows from the engine page, grant the engine window permission separately:
json{ "identifier": "engine-windows", "windows": ["deeprespond-im-engine"], "permissions": ["core:webview:allow-create-webview-window"] }The new window's label (such as
call-*above) also needsdeeprespond-im:window.
Exiting and tray apps
Exit when the last window closes
By default (exit_when_last_window_closed(true)), the plugin exits the app when the last window other than the engine window closes. That includes windows without a client (a splash screen, a settings window); windows that are only hidden don't count as closed.
Before exiting, the plugin lets the engine clean up, waiting up to 2 seconds: it hangs up the user's current call (the other side sees it end immediately), closes the connection normally (contacts see the user go offline immediately), and closes the local database. Anything not done within 2 seconds is cut off; unsent messages stay in the send queue and go out at the next start.
The same happens when the user presses Cmd+Q, or the system shuts down or logs out: the plugin asks the system for a delay and cleans up within it.
Minimize to the tray
For apps that stay in the tray receiving messages after windows close, set exit_when_last_window_closed to false and quit from the tray menu:
// src-tauri/src/lib.rs
use tauri::menu::{Menu, MenuItem};
use tauri::tray::TrayIconBuilder;
use tauri::Manager;
fn show_main<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
if let Some(w) = app.get_webview_window("main") {
let _ = w.unminimize();
let _ = w.show();
let _ = w.set_focus();
}
}
pub fn run() {
let app = tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| {
tauri_plugin_deeprespond_im::handle_args(app, &argv);
show_main(app);
}))
.plugin(
tauri_plugin_deeprespond_im::Builder::new()
.exit_when_last_window_closed(false)
// 托盘图标的提示文字显示未读数
.on_unread_changed(|app, unread| {
if let Some(tray) = app.tray_by_id("main") {
let tip = if unread.badge > 0 { format!("IM({} 条未读)", unread.badge) } else { "IM".to_string() };
let _ = tray.set_tooltip(Some(tip));
}
})
.build(),
)
// 关闭主窗口只是把它隐藏起来
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if window.label() == "main" {
api.prevent_close();
let _ = window.hide();
}
}
})
.setup(|app| {
let show = MenuItem::with_id(app, "show", "显示", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &quit])?;
let mut tray = TrayIconBuilder::with_id("main").menu(&menu).tooltip("IM");
if let Some(icon) = app.default_window_icon() {
tray = tray.icon(icon.clone());
}
tray.on_menu_event(|app, event| match event.id.as_ref() {
"show" => show_main(app),
// 经插件退出:引擎先收尾,再退出
"quit" => tauri_plugin_deeprespond_im::exit(app, 0),
_ => {}
})
.build(app)?;
Ok(())
})
.build(tauri::generate_context!())
.expect("error while building tauri application");
app.run(|_app, _event| {
// macOS 的习惯:点击程序坞图标时重新显示窗口
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Reopen { has_visible_windows: false, .. } = _event {
show_main(_app);
}
});
}- Quit with
tauri_plugin_deeprespond_im::exit(app, code)or Tauri's ownapp.exit(code); both go through the plugin's cleanup first. - Don't call
prevent_exit()unconditionally inRunEvent::ExitRequested. The plugin requests exit again after cleaning up, and if you block it the app can never exit. To stay in the tray, useexit_when_last_window_closed(false)or hide the window as in the example. - If the main window was closed (destroyed), and a notification click or answered call needs it, the plugin calls
on_main_window_missingso you can create it again; see Rust settings. - macOS: closing a window usually doesn't quit the app, and clicking the Dock icon opens a window again, so set it to
falseand handleReopenas in the example. - Windows: once every window is hidden, the taskbar has no badge or flashing; you can flash the tray icon instead. See Tray.
Single instance and multiple AppKeys
Single instance
Only one process may use a given set of local data. A second process doesn't create an engine (im.engineState is failed), and calls in its windows that need the server fail with invalid_state (another_instance). Use Tauri's single-instance plugin, pass the second launch's arguments to tauri_plugin_deeprespond_im::handle_args(app, &argv), and show the existing window; see Run only one instance.
Several AppKeys in one app
That works. The engine page creates an engine client per AppKey, and UI windows create the matching client per AppKey. Local data, sign-in and connections are independent between AppKeys:
import { createDRTauriEngine } from '@deeprespond/im-tauri/engine';
createDRTauriEngine({ appKey: '1575529652#work', apiUrl: 'https://im.example.com' });
createDRTauriEngine({ appKey: '1575529652#support', apiUrl: 'https://im.example.com' });Each AppKey has one signed-in user at a time; to switch accounts, sign out first.
Tokens and local data
Where it's stored
Local data is stored in the current system user's app data directory (separated by identifier in tauri.conf.json), and isn't copied to other computers with Windows roaming profiles:
| System | Directory |
|---|---|
| Windows | %LOCALAPPDATA%\{identifier}\deeprespond_im |
| macOS | ~/Library/Application Support/{identifier}/deeprespond_im |
| Linux | ~/.local/share/{identifier}/deeprespond_im |
It holds one SQLite database per user, send-queue attachments, the encrypted sign-in and the device ID. Temporary files being processed are in the app's cache directory, and ones older than 1 day are deleted at startup. On macOS and Linux only the user can enter this directory.
Whether uninstalling deletes this directory is up to your installer; we recommend asking users whether to delete their local chat history. To delete one user's or every user's local data, use im.auth.clearLocalData(); see the Web SDK API reference.
Sign-in tokens
The sign-in (including tokens) is encrypted with a master key and saved in the data directory, and the master key is kept in the system keychain: the macOS Keychain, Windows Credential Manager, or the Linux Secret Service (GNOME Keyring, KWallet). Only the engine reads and writes it; UI windows can't get the tokens.
When the keychain is unavailable (common on Linux desktops without a running Secret Service), secure_storage_fallback in Rust decides what happens:
| Value | Description |
|---|---|
Fallback::File (default) | The master key is stored in a file in the data directory readable only by the user, with a warning in the log. Any program that can read this user's files can decrypt the tokens, so it's less secure than the keychain. It moves to the keychain automatically once the keychain is available again |
Fallback::None | The master key stays only in memory, so users sign in again every time the app starts. Choose this for apps with high security requirements |
use tauri_plugin_deeprespond_im::Fallback;
let im = tauri_plugin_deeprespond_im::Builder::<tauri::Wry>::new()
.secure_storage_fallback(Fallback::None)
.build();If the user denies keychain access, or the saved sign-in can't be decrypted, the user is treated as signed out. On macOS, when the app's signature changes, the system asks whether to allow access to the previously saved keychain item, so sign with a fixed certificate.
Device ID and device name
- Device ID:
tauri_plus 32 random hex characters, generated on first run and saved in the data directory. On one computer, one system user and one app (identifier) share one device ID, across all of the app's AppKeys. Signing in again on the same computer replaces the previous sign-in instead of using another device slot. - Reinstalling: if the data directory was deleted on uninstall, the reinstalled app is a new device and must sign in again.
- Data directory copied to another computer (such as with macOS Migration Assistant or a full-disk clone): the SDK notices the computer changed, generates a new device ID, and the user signs in again; local chat history is kept. This stops two computers signing in as the same device and pushing each other offline.
- Device name: defaults to the computer name, such as "Zhang San's MacBook Pro", shown in the user's own signed-in device list and invisible to other users. Computer names often include the user's real name; set the engine option
deviceNameif you don't want that.
Encrypt the local database
The database isn't encrypted by default and relies on the system's user isolation. To encrypt it, do two things:
Turn on the plugin's
sqlcipherfeature insrc-tauri/Cargo.toml:tomltauri-plugin-deeprespond-im = { version = "=1.1.1", features = ["sqlcipher"] }Pass
encryptDatabase: truewhen creating the engine.
The database key is generated randomly and saved encrypted, like the sign-in tokens. Passing true without the feature makes creating the engine reject with unsupported (sqlcipher_required) rather than silently not encrypting. The first start after turning encryption on or off deletes and rebuilds the local database and syncs again from the server (emitting storage.reset with reason encryption_changed), and messages not yet sent are lost.
Disk full
When writing the local database fails, calls reject with storage_error (quota_exceeded); when downloading or saving attachments, storage_error (disk_full). At the same time storage.full ({ freed_users }, at most once per 10 minutes) is emitted, and the SDK deletes the local data of the least recently used other users on this computer to free space. When you get it, tell the user to free up disk space.
When the local database is corrupted, or an older version of the app is installed, the SDK deletes the database and syncs again, emitting storage.reset (reason corrupted or schema_downgrade).
Rust settings
The plugin's Builder methods:
| Method | Default | Description |
|---|---|---|
engine_url(path) | "engine.html" | The engine page's path among the front-end assets. A remote address errors at startup (development builds allow the local dev server's address) |
main_window(label) | "main" | The window shown and focused when a notification click or answered call isn't handled by any window |
on_main_window_missing(f) | None | Called when that window doesn't exist, so you can create it again: |app| { ... } |
exit_when_last_window_closed(bool) | true | Exit the app when the last window other than the engine window closes; set to false for tray apps |
secure_storage_fallback(f) | Fallback::File | What to do when the keychain is unavailable; see Sign-in tokens |
log_file(bool), log_dir(path) | false, the app's log directory | Write log files; see Diagnostics and logs |
proxy(url) | System proxy | Proxy for the plugin's file uploads and downloads; see Network and proxies |
allow_read_dir(path), allow_write_dir(path) | None | Extra directories allowed for reading or writing; can be called several times. See Where files can come from |
on_unread_changed(f) | None | Called when the unread count changes: |app, unread| { ... }, where unread.total is the total unread and unread.badge the badge count. Use it to update the tray tooltip or menu |
on_notification_click(f) | None | Called when no window claims a notification click within 300 milliseconds; see Notification clicks |
on_call_action(f) | None | Called when a button on a call notification is clicked: |app, action| { ... }, where action.action is accept or decline and action.call_id is the call ID. See Call notifications |
disable_app_nap(bool) | true | On macOS, prevents App Nap while signed in so the system doesn't slow down heartbeats (it doesn't prevent sleep) |
With only the defaults, you can write tauri_plugin_deeprespond_im::init().
Other Rust APIs:
| API | Description |
|---|---|
tauri_plugin_deeprespond_im::exit(app, code) | Exits the app after cleanup; see Exiting and tray apps |
tauri_plugin_deeprespond_im::handle_args(app, &argv) | Call in the single-instance plugin's callback to pass on notification clicks in the second launch's arguments |
app.deeprespond_im().unread() | The current total unread and badge counts (Unread { total, badge }); needs use tauri_plugin_deeprespond_im::DeeprespondImExt; |
app.deeprespond_im().engine_state() | The engine state (EngineState::Starting, Ready, Restarting, Failed) |
API reference
createDRTauriEngine()
Creates the engine client in the engine page. Import from @deeprespond/im-tauri/engine.
| Parameter | Type | Required | Description |
|---|---|---|---|
options | DRTauriEngineOptions | Yes | See Engine page options |
Returns: DREngine; see below.
Errors (thrown synchronously): unsupported (not_engine_window: not in the engine page; or the WebView doesn't meet the requirements), invalid_state (client_exists), local_validation (bad appKey or apiUrl format). When the plugin and npm package versions differ (version_mismatch), encryption is required without the sqlcipher feature (sqlcipher_required), or another process is using the same data (another_instance), the engine client can't work: the reason is logged, and calls in UI windows fail with the matching error.
DREngine
| Member | Description |
|---|---|
client | The engine client, used like a UI window's client, plus desktop |
onAlert(handler) | With notifications.mode manual, handles new messages and requests when the engine is responsible for alerting; see Show notifications yourself. Returns a function that unregisters it |
onIncomingCall(handler) | With notifications.calls manual, handles incoming calls when the engine is responsible for alerting, receiving a CallHandle; see Handle calls yourself. Returns a function that unregisters it |
createDRTauriClient()
Creates the client in a UI window. Import from @deeprespond/im-tauri.
| Parameter | Type | Required | Description |
|---|---|---|---|
options.appKey | string | Yes | The app's AppKey, the same as in the engine page |
options.logger | { level?: LogLevel; sink?: (entry: LogEntry) => void } | No | This window's logging; also written to the log file when it's on |
Returns: DRTauriClient, which is the Web SDK's DRClient plus desktop and engineState.
Errors (thrown synchronously): unsupported (not_tauri: not in a Tauri app; not_ui_window: in the engine page; or the WebView doesn't meet the requirements), invalid_state (client_exists), local_validation (bad appKey format).
im.engineState
Read-only 'starting' | 'ready' | 'restarting' | 'failed': the engine's current state; see Engine state. engine.stateChanged is emitted when it changes. Only in UI windows.
Standalone channel window lifecycle
The media connection of standalone RTC channels only runs in UI windows; the engine only forwards requests that need the user's sign-in, and the engine window can't join channels. When a window closes, is frozen by the system, the client is destroyed or the engine restarts, the SDK disconnects the media; if the process is force-killed, the server may not be told it left. Minimizing a window doesn't count as leaving.
When the client is destroyed, the SDK waits up to 5 seconds for the channel to leave; this is separate from the cleanup time when the app exits. Test device permissions and WebView capabilities on each target system; for full details, see Standalone channels.
