core.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. /**
  5. * Invoke your custom commands.
  6. *
  7. * This package is also accessible with `window.__TAURI__.core` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
  8. * @module
  9. */
  10. /**
  11. * A key to be used to implement a special function
  12. * on your types that define how your type should be serialized
  13. * when passing across the IPC.
  14. * @example
  15. * Given a type in Rust that looks like this
  16. * ```rs
  17. * #[derive(serde::Serialize, serde::Deserialize)
  18. * enum UserId {
  19. * String(String),
  20. * Number(u32),
  21. * }
  22. * ```
  23. * `UserId::String("id")` would be serialized into `{ String: "id" }`
  24. * and so we need to pass the same structure back to Rust
  25. * ```ts
  26. * import { SERIALIZE_TO_IPC_FN } from "@tauri-apps/api/core"
  27. *
  28. * class UserIdString {
  29. * id
  30. * constructor(id) {
  31. * this.id = id
  32. * }
  33. *
  34. * [SERIALIZE_TO_IPC_FN]() {
  35. * return { String: this.id }
  36. * }
  37. * }
  38. *
  39. * class UserIdNumber {
  40. * id
  41. * constructor(id) {
  42. * this.id = id
  43. * }
  44. *
  45. * [SERIALIZE_TO_IPC_FN]() {
  46. * return { Number: this.id }
  47. * }
  48. * }
  49. *
  50. *
  51. * type UserId = UserIdString | UserIdNumber
  52. * ```
  53. *
  54. */
  55. // if this value changes, make sure to update it in:
  56. // 1. ipc.js
  57. // 2. process-ipc-message-fn.js
  58. export const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__'
  59. /**
  60. * Transforms a callback function to a string identifier that can be passed to the backend.
  61. * The backend uses the identifier to `eval()` the callback.
  62. *
  63. * @return A unique identifier associated with the callback function.
  64. *
  65. * @since 1.0.0
  66. */
  67. function transformCallback<T = unknown>(
  68. callback?: (response: T) => void,
  69. once = false
  70. ): number {
  71. return window.__TAURI_INTERNALS__.transformCallback(callback, once)
  72. }
  73. class Channel<T = unknown> {
  74. id: number
  75. #onmessage: (response: T) => void = () => {
  76. // no-op
  77. }
  78. #nextMessageId = 0
  79. #pendingMessages: Record<string, T> = {}
  80. constructor() {
  81. this.id = transformCallback(
  82. ({ message, id }: { message: T; id: number }) => {
  83. // the id is used as a mechanism to preserve message order
  84. if (id === this.#nextMessageId) {
  85. this.#nextMessageId = id + 1
  86. this.#onmessage(message)
  87. // process pending messages
  88. const pendingMessageIds = Object.keys(this.#pendingMessages)
  89. if (pendingMessageIds.length > 0) {
  90. let nextId = id + 1
  91. for (const pendingId of pendingMessageIds.sort()) {
  92. // if we have the next message, process it
  93. if (parseInt(pendingId) === nextId) {
  94. // eslint-disable-next-line security/detect-object-injection
  95. const message = this.#pendingMessages[pendingId]
  96. // eslint-disable-next-line security/detect-object-injection
  97. delete this.#pendingMessages[pendingId]
  98. this.#onmessage(message)
  99. // move the id counter to the next message to check
  100. nextId += 1
  101. } else {
  102. // we do not have the next message, let's wait
  103. break
  104. }
  105. }
  106. this.#nextMessageId = nextId
  107. }
  108. } else {
  109. this.#pendingMessages[id.toString()] = message
  110. }
  111. }
  112. )
  113. }
  114. set onmessage(handler: (response: T) => void) {
  115. this.#onmessage = handler
  116. }
  117. get onmessage(): (response: T) => void {
  118. return this.#onmessage
  119. }
  120. [SERIALIZE_TO_IPC_FN]() {
  121. return `__CHANNEL__:${this.id}`
  122. }
  123. toJSON(): string {
  124. // eslint-disable-next-line security/detect-object-injection
  125. return this[SERIALIZE_TO_IPC_FN]()
  126. }
  127. }
  128. class PluginListener {
  129. plugin: string
  130. event: string
  131. channelId: number
  132. constructor(plugin: string, event: string, channelId: number) {
  133. this.plugin = plugin
  134. this.event = event
  135. this.channelId = channelId
  136. }
  137. async unregister(): Promise<void> {
  138. return invoke(`plugin:${this.plugin}|remove_listener`, {
  139. event: this.event,
  140. channelId: this.channelId
  141. })
  142. }
  143. }
  144. /**
  145. * Adds a listener to a plugin event.
  146. *
  147. * @returns The listener object to stop listening to the events.
  148. *
  149. * @since 2.0.0
  150. */
  151. async function addPluginListener<T>(
  152. plugin: string,
  153. event: string,
  154. cb: (payload: T) => void
  155. ): Promise<PluginListener> {
  156. const handler = new Channel<T>()
  157. handler.onmessage = cb
  158. return invoke(`plugin:${plugin}|register_listener`, { event, handler }).then(
  159. () => new PluginListener(plugin, event, handler.id)
  160. )
  161. }
  162. type PermissionState = 'granted' | 'denied' | 'prompt' | 'prompt-with-rationale'
  163. /**
  164. * Get permission state for a plugin.
  165. *
  166. * This should be used by plugin authors to wrap their actual implementation.
  167. */
  168. async function checkPermissions<T>(plugin: string): Promise<T> {
  169. return invoke(`plugin:${plugin}|check_permissions`)
  170. }
  171. /**
  172. * Request permissions.
  173. *
  174. * This should be used by plugin authors to wrap their actual implementation.
  175. */
  176. async function requestPermissions<T>(plugin: string): Promise<T> {
  177. return invoke(`plugin:${plugin}|request_permissions`)
  178. }
  179. /**
  180. * Command arguments.
  181. *
  182. * @since 1.0.0
  183. */
  184. type InvokeArgs = Record<string, unknown> | number[] | ArrayBuffer | Uint8Array
  185. /**
  186. * @since 2.0.0
  187. */
  188. interface InvokeOptions {
  189. headers: Headers | Record<string, string>
  190. }
  191. /**
  192. * Sends a message to the backend.
  193. * @example
  194. * ```typescript
  195. * import { invoke } from '@tauri-apps/api/core';
  196. * await invoke('login', { user: 'tauri', password: 'poiwe3h4r5ip3yrhtew9ty' });
  197. * ```
  198. *
  199. * @param cmd The command name.
  200. * @param args The optional arguments to pass to the command.
  201. * @param options The request options.
  202. * @return A promise resolving or rejecting to the backend response.
  203. *
  204. * @since 1.0.0
  205. */
  206. async function invoke<T>(
  207. cmd: string,
  208. args: InvokeArgs = {},
  209. options?: InvokeOptions
  210. ): Promise<T> {
  211. return window.__TAURI_INTERNALS__.invoke(cmd, args, options)
  212. }
  213. /**
  214. * Convert a device file path to an URL that can be loaded by the webview.
  215. * Note that `asset:` and `http://asset.localhost` must be added to [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1) in `tauri.conf.json`.
  216. * Example CSP value: `"csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost"` to use the asset protocol on image sources.
  217. *
  218. * Additionally, `"enable" : "true"` must be added to [`app.security.assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig)
  219. * in `tauri.conf.json` and its access scope must be defined on the `scope` array on the same `assetProtocol` object.
  220. *
  221. * @param filePath The file path.
  222. * @param protocol The protocol to use. Defaults to `asset`. You only need to set this when using a custom protocol.
  223. * @example
  224. * ```typescript
  225. * import { appDataDir, join } from '@tauri-apps/api/path';
  226. * import { convertFileSrc } from '@tauri-apps/api/core';
  227. * const appDataDirPath = await appDataDir();
  228. * const filePath = await join(appDataDirPath, 'assets/video.mp4');
  229. * const assetUrl = convertFileSrc(filePath);
  230. *
  231. * const video = document.getElementById('my-video');
  232. * const source = document.createElement('source');
  233. * source.type = 'video/mp4';
  234. * source.src = assetUrl;
  235. * video.appendChild(source);
  236. * video.load();
  237. * ```
  238. *
  239. * @return the URL that can be used as source on the webview.
  240. *
  241. * @since 1.0.0
  242. */
  243. function convertFileSrc(filePath: string, protocol = 'asset'): string {
  244. return window.__TAURI_INTERNALS__.convertFileSrc(filePath, protocol)
  245. }
  246. /**
  247. * A rust-backed resource stored through `tauri::Manager::resources_table` API.
  248. *
  249. * The resource lives in the main process and does not exist
  250. * in the Javascript world, and thus will not be cleaned up automatiacally
  251. * except on application exit. If you want to clean it up early, call {@linkcode Resource.close}
  252. *
  253. * @example
  254. * ```typescript
  255. * import { Resource, invoke } from '@tauri-apps/api/core';
  256. * export class DatabaseHandle extends Resource {
  257. * static async open(path: string): Promise<DatabaseHandle> {
  258. * const rid: number = await invoke('open_db', { path });
  259. * return new DatabaseHandle(rid);
  260. * }
  261. *
  262. * async execute(sql: string): Promise<void> {
  263. * await invoke('execute_sql', { rid: this.rid, sql });
  264. * }
  265. * }
  266. * ```
  267. */
  268. export class Resource {
  269. readonly #rid: number
  270. get rid(): number {
  271. return this.#rid
  272. }
  273. constructor(rid: number) {
  274. this.#rid = rid
  275. }
  276. /**
  277. * Destroys and cleans up this resource from memory.
  278. * **You should not call any method on this object anymore and should drop any reference to it.**
  279. */
  280. async close(): Promise<void> {
  281. return invoke('plugin:resources|close', {
  282. rid: this.rid
  283. })
  284. }
  285. }
  286. function isTauri(): boolean {
  287. return 'isTauri' in window && !!window.isTauri
  288. }
  289. export type { InvokeArgs, InvokeOptions }
  290. export {
  291. transformCallback,
  292. Channel,
  293. PluginListener,
  294. addPluginListener,
  295. PermissionState,
  296. checkPermissions,
  297. requestPermissions,
  298. invoke,
  299. convertFileSrc,
  300. isTauri
  301. }