tuffite0.1
契约与细节

JavaScript 与 ShellAPI

直接源自代码的原生门面与声明契约。

实验性源码版本。生产安全加固与 SDK 发布仍在进行中。

使用生成的原生门面#

原生方法返回 Promise,并要求匹配 origin 的能力授权。下面的契约从 Framework 已提交的 TypeScript 声明中生成。bigint、不透明句柄和二进制载荷保持声明的编解码语义。

javascript
import '/tuffite-shell-api.js';
const windows = await window.tuffite.window.list();

window.tuffite 桥接对象#

原生运行时在符合条件的应用和 Explorer 主文档中提供 window.tuffite。NativeBridge 契约包含 invoke()、postMessage()、addEventListener() 和 removeEventListener()。生成模块在同一对象上扩展 tuffite.window 等原生门面。普通浏览器标签页不提供原生桥接对象。

订阅与移除事件监听#

监听使用 EventTarget API。addEventListener(type, listener, options?) 接受 capture 布尔值或 AddEventListenerOptions,包括 once 和 signal。removeEventListener(type, listener, options?) 须使用相同函数和匹配的 capture 值。在 React 中可在 effect 内订阅,并在清理函数中移除监听。

  • channel:原生应用事件,detail 为 {channel, data: Uint8Array}。按应用事件契约解码 data,不会自动转换成 JSON。
  • message:Explorer 树的 JSON 消息,detail 为 {data, from}。名为 message 的业务 channel 仍通过 channel 事件类型接收。
  • navigation、open、popup、newwindow:冒泡的 Explorer 请求,detail 为 {url, disposition, userGesture, from}。dialog:带 accept() 和 dismiss() 的 ExplorerDialogController。传播和归属规则参见 Explorer API。
javascript
const onChannel = (event) => {
  const { channel, data } = event.detail; // data: Uint8Array
  if (channel === 'app:changed') {
    console.log(channel, data);
  }
};
window.tuffite.addEventListener('channel', onChannel);

// During component cleanup, use the same listener reference.
window.tuffite.removeEventListener('channel', onChannel);

发送 Explorer 消息#

postMessage(data, {to}?) 返回 Promise<boolean>。默认目标为 top,也接受 parent 和浏览器签发的表面引用。data 须能序列化为 JSON。浏览器验证路由和归属。true 表示接受并投递或入队,不表示接收方应答。

javascript
await window.tuffite.postMessage({ type: 'ready' }, { to: 'parent' });
const onMessage = ({ detail: { data, from } }) => {
  console.log(data, from);
};
window.tuffite.addEventListener('message', onMessage);
// Later:
window.tuffite.removeEventListener('message', onMessage);

底层二进制调用#

invoke(channel, payload) 返回 Promise<Uint8Array>,并要求对应路由的原生权限。它不经过 Explorer 树,不接受 to 选项。普通应用调用应使用生成的类型化门面,它们处理命令标识、编解码器、schema 指纹和协议验证。

javascript
const response = await window.tuffite.invoke(route, binaryPayload);
// binaryPayload and response are Uint8Array.

运行时辅助与错误#

tuffite/runtime 导出 TuffiteError、invokeBinary()、createCommandApi()、createTypedCommandApi() 和 createFrameworkApi(),以及 codec 和桥接类型。这些辅助接口服务于生成绑定和工具。TuffiteError 提供 code 以及可选 requestId/cause。原生应用包以 /tuffite-runtime.js 提供运行时模块。

JavaScript 包导出#

下面的完整声明从 JavaScript 包 exports 中的每个 types 入口生成,包含 NativeBridge 事件重载、运行时辅助、Explorer 类型和仅用于开发的 Vite 插件。可按导出路径、类型或成员名称筛选。

tuffite
typescript
// Generated from ../../framework/shell_api/common.d.ts, ../../framework/shell_api/dialog.d.ts, ../../framework/shell_api/path.d.ts, ../../framework/shell_api/remoting.d.ts, ../../framework/shell_api/fs.d.ts, ../../framework/shell_api/storage.d.ts, ../../framework/shell_api/log.d.ts, ../../framework/shell_api/network.d.ts, ../../framework/shell_api/utility.d.ts, ../../framework/shell_api/window.d.ts. Do not edit.

declare global {
  interface Window {
    readonly tuffite: typeof import('./dialog').tuffite & typeof import('./path').tuffite & typeof import('./remoting').tuffite & typeof import('./fs').tuffite & typeof import('./storage').tuffite & typeof import('./log').tuffite & typeof import('./network').tuffite & typeof import('./utility').tuffite & typeof import('./window').tuffite & import('../runtime').NativeBridge;
  }
}

export {};
查看源码
tuffite/runtime
typescript
export type Codec = Readonly<Record<string, unknown>>;

export type TuffiteErrorCode =
  | "invalid_argument"
  | "incompatible_abi"
  | "invalid_handle"
  | "unavailable"
  | "resource_exhausted"
  | "internal"
  | "transport_unavailable"
  | "invalid_request"
  | "unknown_command"
  | "handler_failed"
  | "decode_error"
  | "protocol_mismatch"
  | "schema_mismatch"
  | "command_failed"
  | (string & {});

export class TuffiteError extends Error {
  readonly code: TuffiteErrorCode;
  readonly requestId?: bigint;
  readonly cause?: unknown;
  constructor(
    code: TuffiteErrorCode,
    message: string,
    options?: Readonly<{ requestId?: bigint; cause?: unknown }>,
  );
}

export interface ChannelEvent extends Event {
  readonly detail: Readonly<{ channel: string; data: Uint8Array }>;
}

export interface ExplorerMessageEvent<T = unknown> extends Event {
  readonly detail: Readonly<{ data: T; from: string }>;
}

export interface ExplorerOpenEvent extends Event {
  readonly detail: Readonly<{
    url: string;
    disposition: string;
    userGesture: boolean;
    from: string;
  }>;
}

export interface ExplorerDialogController {
  readonly type: "alert" | "confirm" | "prompt" | "beforeunload";
  readonly message: string;
  readonly defaultValue: string;
  readonly isReload: boolean;
  readonly from: string;
  accept(promptText?: string): Promise<boolean>;
  dismiss(): Promise<boolean>;
}

export interface ExplorerDialogEvent extends Event {
  readonly detail: ExplorerDialogController;
}

export type ExplorerSurfaceRef = string & {
  readonly __tuffiteExplorerSurfaceRef: unique symbol;
};
export type ExplorerMessageTarget = "parent" | "top" | ExplorerSurfaceRef;

export interface NativeBridge extends EventTarget {
  invoke(channel: string, payload: Uint8Array): Promise<Uint8Array>;
  postMessage(
    data: unknown,
    options?: Readonly<{ to?: ExplorerMessageTarget }>,
  ): Promise<boolean>;
  addEventListener(
    type: "channel",
    listener: (event: ChannelEvent) => void,
    options?: boolean | AddEventListenerOptions,
  ): void;
  removeEventListener(
    type: "channel",
    listener: (event: ChannelEvent) => void,
    options?: boolean | EventListenerOptions,
  ): void;
  addEventListener(
    type: "message",
    listener: (event: ExplorerMessageEvent) => void,
    options?: boolean | AddEventListenerOptions,
  ): void;
  removeEventListener(
    type: "message",
    listener: (event: ExplorerMessageEvent) => void,
    options?: boolean | EventListenerOptions,
  ): void;
  addEventListener(
    type: "open" | "popup" | "newwindow" | "navigation",
    listener: (event: ExplorerOpenEvent) => void,
    options?: boolean | AddEventListenerOptions,
  ): void;
  removeEventListener(
    type: "open" | "popup" | "newwindow" | "navigation",
    listener: (event: ExplorerOpenEvent) => void,
    options?: boolean | EventListenerOptions,
  ): void;
  addEventListener(
    type: "dialog",
    listener: (event: ExplorerDialogEvent) => void,
    options?: boolean | AddEventListenerOptions,
  ): void;
  removeEventListener(
    type: "dialog",
    listener: (event: ExplorerDialogEvent) => void,
    options?: boolean | EventListenerOptions,
  ): void;
}

export type CommandEntry = readonly [
  path: string,
  id: string,
  schemaFingerprint: string,
  request: readonly Codec[],
  response: Codec,
];

export type ConstantEntry = readonly [path: string, value: unknown];

export function invokeBinary(
  channel: string,
  commandId: bigint,
  schemaFingerprint: bigint,
  payload?: Uint8Array,
): Promise<Uint8Array>;

export function createCommandApi(
  entries: readonly (readonly [path: string, id: bigint])[],
  typed?: boolean,
  rootTarget?: object,
  channelPrefix?: string,
): object;

export function createTypedCommandApi(
  entries: readonly CommandEntry[],
  constants?: readonly ConstantEntry[],
  namespace?: string,
): object;

export type FrameworkEntry = readonly [
  path: string,
  request: readonly Codec[],
  response: Codec,
];

export function createFrameworkApi(
  entries: readonly FrameworkEntry[],
  constants?: readonly ConstantEntry[],
): object;
查看源码
tuffite/explorer
typescript
export type ExplorerNavigationPolicy = "emit" | "allow";
export type ExplorerDialogPolicy = "emit" | "suppress";
export type ExplorerJavaScriptWorld = "isolated" | "main";
export type ExplorerPartitionMode = "shared" | "persist" | "memory";

export interface ExplorerNavigationState {
  readonly url: string;
  readonly title: string;
  readonly documentId: string;
  readonly canGoBack: boolean;
  readonly canGoForward: boolean;
  readonly isLoading: boolean;
  readonly rendererProcess: Readonly<{
    id: number;
    ownerId: number;
    isolated: boolean;
  }>;
  readonly storagePartition: Readonly<{
    domain: string;
    name: string;
    inMemory: boolean;
    isDefault: boolean;
    mode: ExplorerPartitionMode;
    shared: boolean;
    inherited: boolean;
    requested: string;
    createdDomain: string;
    createdName: string;
    createdInMemory: boolean;
    createdIsDefault: boolean;
  }>;
}

export interface ExplorerLoadErrorState extends ExplorerNavigationState {
  readonly errorCode: number;
  readonly errorDescription: string;
}

export interface ExplorerOpenDetail {
  readonly url: string;
  readonly disposition: string;
  readonly userGesture: boolean;
  readonly from: string;
}

export interface ExplorerDialogController {
  readonly type: "alert" | "confirm" | "prompt" | "beforeunload";
  readonly message: string;
  readonly defaultValue: string;
  readonly isReload: boolean;
  readonly from: string;
  accept(promptText?: string): Promise<boolean>;
  dismiss(): Promise<boolean>;
}

export type ExplorerTransferState = "pending" | "attaching" | "transferred" | "closed";

/** Pending resources are owned by the source document, independently of GC. */
export interface ExplorerTransfer extends EventTarget {
  readonly token: string;
  readonly state: ExplorerTransferState;
  close(): Promise<void>;
  onstatechange: ((event: Event) => void) | null;
}

export interface ExplorerEventMap {
  download: CustomEvent<ExplorerDownload>;
  attached: Event;
  detached: Event;
  loadstart: CustomEvent<ExplorerNavigationState>;
  loadstop: CustomEvent<ExplorerNavigationState>;
  loadcommit: CustomEvent<ExplorerNavigationState>;
  loaderror: CustomEvent<ExplorerLoadErrorState>;
  open: CustomEvent<ExplorerOpenDetail>;
  popup: CustomEvent<ExplorerOpenDetail>;
  newwindow: CustomEvent<ExplorerOpenDetail>;
  navigation: CustomEvent<ExplorerOpenDetail>;
  dialog: CustomEvent<ExplorerDialogController>;
  message: CustomEvent<unknown>;
}

export interface HTMLExplorerElement extends HTMLElement {
  src: string;
  /** Empty selects the parent's partition; `shared` is not an element value. */
  partition: string;
  navigation: ExplorerNavigationPolicy;
  dialogs: ExplorerDialogPolicy;
  contextMenuEnabled: boolean;
  deferred: boolean;
  detach(): Promise<ExplorerTransfer>;
  attach(token: string): Promise<void>;
  ondetached: ((event: Event) => void) | null;
  onattached: ((event: Event) => void) | null;
  /** Starts navigation; completion is reported by load events. */
  load(url: string): void;
  download(url: string): Promise<boolean>;
  getDownloads(): Promise<ExplorerDownload[]>;
  cancelDownload(id: number): Promise<boolean>;
  /** Base64 bytes of an owned completed download, bounded to 64 MiB. */
  readDownload(id: number): Promise<string>;
  /** A compositor PNG data URL, or an empty string when unavailable. */
  capturePreview(): Promise<string>;
  back(): Promise<boolean>;
  forward(): Promise<boolean>;
  go(offset: number): Promise<boolean>;
  reload(options?: Readonly<{ bypassCache?: boolean }>): Promise<boolean>;
  stop(): Promise<boolean>;
  getNavigationState(): Promise<ExplorerNavigationState>;
  /** The caller must validate the JSON-representable result at runtime. */
  executeJavaScript(
    source: string,
    options?: Readonly<{
      world?: ExplorerJavaScriptWorld;
      expectedDocumentId?: string;
    }>,
  ): Promise<unknown>;
  postMessage(data: unknown): Promise<boolean>;
  addEventListener<K extends keyof ExplorerEventMap>(
    type: K,
    listener: (this: HTMLExplorerElement, event: ExplorerEventMap[K]) => void,
    options?: boolean | AddEventListenerOptions,
  ): void;
  removeEventListener<K extends keyof ExplorerEventMap>(
    type: K,
    listener: (this: HTMLExplorerElement, event: ExplorerEventMap[K]) => void,
    options?: boolean | EventListenerOptions,
  ): void;
}

declare global {
  interface HTMLElementTagNameMap {
    explorer: HTMLExplorerElement;
  }
}

export type ExplorerDownload = {
  id: number;
  url: string;
  filename: string;
  path: string;
  mimeType: string;
  state: "in-progress" | "complete" | "cancelled" | "interrupted";
  receivedBytes: number;
  totalBytes: number;
  error: number;
  navigation: boolean;
};
查看源码
tuffite/vite
typescript
export interface TuffiteViteOptions {
  manifest?: string;
  root?: string;
  static?: Readonly<Record<string, string>>;
  shellApi?: false | Readonly<{ url: string; file: string }>;
}

export function tuffite(options?: TuffiteViteOptions): import("vite").Plugin;
export function findManifest(start: string): string | undefined;
export function contentTypeForPath(path: string): string | undefined;
export function decodeRequestPath(requestUrl: string | undefined): string | undefined;
export function resolveConfinedFile(root: string, path: string): Promise<string | undefined>;
查看源码

Framework 模块#

展开模块可以查看完整源码声明,包括参数和返回值模型。

tuffite.common0 个方法
typescript
/** Filesystem roots accepted by path and fs APIs. */
export enum BaseDirectory {
  Resource = "resource",
  Data = "data",
  Cache = "cache",
  Temp = "temp",
}
tuffite.dialog2 个方法
openFile(options?: OpenFileDialogOptions): Promise<string | undefined>;
openDirectory(options?: OpenDirectoryDialogOptions): Promise<string | undefined>;
typescript
export interface OpenFileDialogOptions {
  readonly title?: string;
  readonly defaultPath?: string;
  /** Extension groups without leading dots, for example [["html", "htm"], ["md"]]. */
  readonly extensions?: string[][];
}

export interface OpenDirectoryDialogOptions {
  readonly title?: string;
}

export declare namespace tuffite {
  export namespace dialog {
    export function openFile(options?: OpenFileDialogOptions): Promise<string | undefined>;
    export function openDirectory(options?: OpenDirectoryDialogOptions): Promise<string | undefined>;
  }
}
tuffite.fs5 个方法
readFile(base: BaseDirectory, path: string): Promise<Uint8Array>;
writeFile( base: BaseDirectory, path: string, contents: Uint8Array, ): Promise<void>;
exists(base: BaseDirectory, path: string): Promise<boolean>;
createDir(base: BaseDirectory, path: string): Promise<void>;
removeFile(base: BaseDirectory, path: string): Promise<void>;
typescript
export declare namespace tuffite {
  export namespace fs {
    export function readFile(base: BaseDirectory, path: string): Promise<Uint8Array>;
    export function writeFile(
      base: BaseDirectory,
      path: string,
      contents: Uint8Array,
    ): Promise<void>;
    export function exists(base: BaseDirectory, path: string): Promise<boolean>;
    export function createDir(base: BaseDirectory, path: string): Promise<void>;
    export function removeFile(base: BaseDirectory, path: string): Promise<void>;
  }
}
tuffite.log5 个方法
trace(message: string): Promise<void>;
debug(message: string): Promise<void>;
info(message: string): Promise<void>;
warn(message: string): Promise<void>;
error(message: string): Promise<void>;
typescript
export declare namespace tuffite {
  export namespace log {
    export function trace(message: string): Promise<void>;
    export function debug(message: string): Promise<void>;
    export function info(message: string): Promise<void>;
    export function warn(message: string): Promise<void>;
    export function error(message: string): Promise<void>;
  }
}
tuffite.network1 个方法
request(url: string): Promise<NetworkResponse>;
typescript
export interface NetworkResponse {
  readonly status: number;
  readonly body: Uint8Array;
}

export declare namespace tuffite {
  export namespace net {
    export function request(url: string): Promise<NetworkResponse>;
  }
}
tuffite.path2 个方法
directories(): Promise<PathDirectories>;
resolve(base: BaseDirectory, relative?: string): Promise<string>;
typescript
export interface PathDirectories {
  readonly resource: string;
  readonly data: string;
  readonly cache: string;
  readonly temp: string;
}

export declare namespace tuffite {
  export namespace path {
    export function directories(): Promise<PathDirectories>;
    export function resolve(base: BaseDirectory, relative?: string): Promise<string>;
  }
}
tuffite.remoting9 个方法
capabilities(): Promise<RemotingCapabilities>;
start(options: RemotingStartOptions): Promise<RemotingSession>;
processSignal(id: bigint, signal: RemotingSignal): Promise<void>;
takeSignals(id: bigint, afterSequence?: bigint): Promise<RemotingSignal[]>;
sendInput(id: bigint, event: RemotingInputEvent): Promise<void>;
stop(id: bigint): Promise<void>;
stats(id: bigint): Promise<RemotingStats>;
capture(screenId?: bigint): Promise<RemotingSnapshot>;
injectInput(event: RemotingInputEvent): Promise<void>;
typescript
export interface RemotingCapabilities {
  readonly available: boolean;
  readonly protocol: string;
  readonly videoCodecs: string[];
  readonly input: boolean;
  readonly clipboard: boolean;
  readonly reconnect: boolean;
  readonly snapshot: boolean;
}

export interface RemotingIceServer {
  readonly urls: string[];
  readonly username?: string;
  readonly credential?: string;
}

export interface RemotingStartOptions {
  /** Per-session secret supplied by the application's authenticated control plane. */
  readonly authKey: string;
  readonly iceServers: RemotingIceServer[];
  readonly maxBitrateKbps?: number;
  readonly targetFramerate?: number;
  readonly screenId?: bigint;
}

export interface RemotingSession {
  readonly id: bigint;
  readonly state: string;
}

export interface RemotingSessionDescription {
  readonly type: string;
  readonly sdp: string;
}

export interface RemotingIceCandidate {
  readonly mid: string;
  readonly mLineIndex: number;
  readonly candidate: string;
}

export interface RemotingSignal {
  readonly sequence: bigint;
  readonly description?: RemotingSessionDescription;
  readonly candidates: RemotingIceCandidate[];
}

export interface RemotingInputEvent {
  readonly kind: string;
  readonly pressed?: boolean;
  readonly usbKeycode?: number;
  readonly text?: string;
  readonly x?: number;
  readonly y?: number;
  readonly button?: string;
  readonly buttonDown?: boolean;
  readonly wheelDeltaX?: number;
  readonly wheelDeltaY?: number;
  readonly wheelTicksX?: number;
  readonly wheelTicksY?: number;
  readonly clipboardMimeType?: string;
  readonly clipboardData?: Uint8Array;
}

export interface RemotingStats {
  readonly state: string;
  readonly route: string;
  readonly transport: string;
  readonly latencyMs: number;
  readonly jitterMs: number;
  readonly availableBandwidthKbps: number;
  readonly packetLossPercent: number;
}

export interface RemotingSnapshot {
  readonly width: number;
  readonly height: number;
  readonly mimeType: string;
  readonly data: Uint8Array;
}

export declare namespace tuffite {
  export namespace remoting {
    export function capabilities(): Promise<RemotingCapabilities>;
    export function start(options: RemotingStartOptions): Promise<RemotingSession>;
    export function processSignal(id: bigint, signal: RemotingSignal): Promise<void>;
    export function takeSignals(id: bigint, afterSequence?: bigint): Promise<RemotingSignal[]>;
    export function sendInput(id: bigint, event: RemotingInputEvent): Promise<void>;
    export function stop(id: bigint): Promise<void>;
    export function stats(id: bigint): Promise<RemotingStats>;
    export function capture(screenId?: bigint): Promise<RemotingSnapshot>;
    /** Native application callers only; renderer calls are denied by routing policy. */
    export function injectInput(event: RemotingInputEvent): Promise<void>;
  }
}
tuffite.storage4 个方法
get(key: string): Promise<Uint8Array | undefined>;
set(key: string, value: Uint8Array): Promise<void>;
remove(key: string): Promise<void>;
clear(): Promise<void>;
typescript
export declare namespace tuffite {
  export namespace storage {
    export function get(key: string): Promise<Uint8Array | undefined>;
    export function set(key: string, value: Uint8Array): Promise<void>;
    export function remove(key: string): Promise<void>;
    export function clear(): Promise<void>;
  }
}
tuffite.utility2 个方法
invoke( name: string, requestId: bigint, payload?: Uint8Array, ): Promise<Uint8Array>;
cancel(name: string, requestId: bigint): Promise<void>;
typescript
export declare namespace tuffite {
  export namespace utility {
    export function invoke(
      name: string,
      requestId: bigint,
      payload?: Uint8Array,
    ): Promise<Uint8Array>;
    export function cancel(name: string, requestId: bigint): Promise<void>;
  }
}
tuffite.window12 个方法
current(): Promise<WindowInfo>;
list(): Promise<WindowInfo[]>;
create(options?: WindowOptions): Promise<WindowInfo>;
focus(id?: WindowId): Promise<WindowInfo>;
minimize(id?: WindowId): Promise<WindowInfo>;
maximize(id?: WindowId): Promise<WindowInfo>;
restore(id?: WindowId): Promise<WindowInfo>;
setFullscreen(fullscreen: boolean, id?: WindowId): Promise<WindowInfo>;
close(id?: WindowId): Promise<void>;
setTitle(title: string, id?: WindowId): Promise<WindowInfo>;
getBounds(id?: WindowId): Promise<WindowBounds>;
setBounds(bounds: WindowBounds, id?: WindowId): Promise<WindowInfo>;
typescript
/** Opaque browser-process identifier. It is intentionally not a native handle. */
export type WindowId = bigint;

export interface WindowBounds {
  readonly x: number;
  readonly y: number;
  readonly width: number;
  readonly height: number;
}

export interface WindowInfo {
  readonly id: WindowId;
  readonly label: string;
  readonly title: string;
  readonly url: string;
  readonly bounds: WindowBounds;
  readonly focused: boolean;
  readonly minimized: boolean;
  readonly maximized: boolean;
  readonly fullscreen: boolean;
}

export interface WindowOptions {
  readonly label?: string;
  readonly url?: string;
  readonly title?: string;
  readonly bounds?: WindowBounds;
  readonly focused?: boolean;
}

export declare namespace tuffite {
  export namespace window {
    export function current(): Promise<WindowInfo>;
    export function list(): Promise<WindowInfo[]>;
    export function create(options?: WindowOptions): Promise<WindowInfo>;
    export function focus(id?: WindowId): Promise<WindowInfo>;
    export function minimize(id?: WindowId): Promise<WindowInfo>;
    export function maximize(id?: WindowId): Promise<WindowInfo>;
    export function restore(id?: WindowId): Promise<WindowInfo>;
    export function setFullscreen(fullscreen: boolean, id?: WindowId): Promise<WindowInfo>;
    export function close(id?: WindowId): Promise<void>;
    export function setTitle(title: string, id?: WindowId): Promise<WindowInfo>;
    export function getBounds(id?: WindowId): Promise<WindowBounds>;
    export function setBounds(bounds: WindowBounds, id?: WindowId): Promise<WindowInfo>;
  }
}