core.ts 7.8 KB

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