zudo-text

検索したい単語を入力

いつでも検索バーを開ける

@takazudo/backend-bridge

Abstracts the backend communication layer so the frontend works with both the real Tauri backend and a mock backend for tests and Storybook. The adapter pattern allows swapping the backend without changing any UI code.

Architecture

Frontend Components
        │
        ▼
   getBackend()  ←── initBackend(adapter)
        │
        ├── TauriAdapter  (native IPC + encrypted cloud workspace)
        ├── MockAdapter   (in-memory, for tests & Storybook)
        └── RestAdapter   (cloud workspace or local HTTP/SSE dev fallback)

Note

messages, pins, notes, and inbox are bridge-level domain names, not proof that same-named Tauri commands exist. In the shipped cloud-primary Tauri adapter, document operations execute against the encrypted workspace model. The matching Rust filesystem engine and HTTP/SSE routes are retained forpnpm dev:rest when no cloud workspace is armed; the renderer does not invokemessages_*, notes_*, or inbox_* filesystem commands. getProjectRoot()reports the compatibility root used by surviving local-only surfaces, not an editable cloud workspace directory.

Subpath Exports

// Main: types + init/get functions
import { initBackend, getBackend } from "@takazudo/backend-bridge";
import { defaultCloudSyncStatus, defaultAuthState } from "@takazudo/backend-bridge";

// Tauri adapter (subpath — not in main index.ts exports)
import { createTauriAdapter } from "@takazudo/backend-bridge/tauri-adapter";

// Mock adapter (for tests & Storybook)
import { createMockAdapter } from "@takazudo/backend-bridge";

// REST adapter (for browser dev with real backend)
import { createRestAdapter } from "@takazudo/backend-bridge";

initBackend / getBackend

The singleton pattern ensures the backend is initialized once at app startup and accessed consistently everywhere.

function initBackend(adapter: BackendAPI): void;
function getBackend(): BackendAPI;

Usage

// At app startup
import { initBackend } from "@takazudo/backend-bridge";
import { createTauriAdapter } from "@takazudo/backend-bridge/tauri-adapter";

initBackend(createTauriAdapter());

// In any component
import { getBackend } from "@takazudo/backend-bridge";

const backend = getBackend();
const messages = await backend.messages.list();

BackendAPI Interface

The BackendAPI interface defines the complete contract between frontend and backend:

interface BackendAPI {
  /** Static set of capabilities for this adapter. */
  capabilities: Capabilities;

  /** Return the runtime role: "root" (ROOT app) or "leaf" (generated LEAF app). */
  appMode: () => Promise<AppRole>;

  /** Low-level file I/O for arbitrary absolute paths (e.g. drag-and-drop import). */
  files: {
    readText: (sourcePath: string) => Promise<string>;
    writeText: (sourcePath: string, content: string) => Promise<void>;
    deleteFile: (path: string) => Promise<void>;
    mkdir: (dir: string) => Promise<void>;
    getUserSkillsDir: () => Promise<string>;
    readDir: (path: string) => Promise<string[]>;
    watchDir: (path: string, watcherId: string) => Promise<void>;
    unwatchDir: (watcherId: string) => Promise<void>;
    onSkillsChanged: (
      watcherId: string,
      callback: (filePath: string, kind: "added" | "updated" | "removed") => void,
    ) => () => void;
  };

  messages: {
    list: () => Promise<MessageMeta[]>;
    /** Light listing: frontmatter + fs metadata only — no body or snippet. */
    listLight: () => Promise<MessageMeta[]>;
    listWithBody: () => Promise<MessageWithBody[]>;
    read: (filename: string) => Promise<string | null>;
    write: (filename: string, content: string) => Promise<boolean>;
    delete: (filename: string) => Promise<void>;
    create: (name: string, content: string) => Promise<string>;
    /** Create a message in a directory relative to the workspace root (e.g. "/archives/"). */
    createInDir: (relDir: string, name: string, content: string) => Promise<string>;
    rename: (oldFilename: string, newFilename: string) => Promise<string>;
    onChanged: (callback: (filename?: string) => void) => () => void;
  };

  pins: {
    list: (pinIndex: number) => Promise<PinEntry[]>;
    /** Fetch immediate children of a directory within a pin (lazy expansion). */
    listChildren: (pinIndex: number, dirPath: string) => Promise<PinEntry[]>;
    read: (pinIndex: number, entryPath: string) => Promise<string | null>;
    write: (pinIndex: number, entryPath: string, content: string) => Promise<boolean>;
    delete: (pinIndex: number, entryPath: string) => Promise<boolean>;
    watchFile: (pinIndex: number, entryPath: string) => Promise<boolean>;
    unwatchFile: () => Promise<boolean>;
    onFileChanged: (callback: (entryPath: string) => void) => () => void;
    /** Create a new file in the pin directory; returns the relative filename. */
    createFile: (pinIndex: number) => Promise<string>;
    /**
     * Write a new file only if the path does not already exist.
     * Rejects with WriteNewFileError on collision or validation failure.
     */
    writeNewFile: (pinIndex: number, filename: string, content: string) => Promise<WriteNewFileResult>;
  };

  /** W9.1 — frame pop-out / dock-back command surface. */
  framePopout: {
    popOut: (frameId: string, providerId: string, propsJson: string) => Promise<string>;
    dock: (windowLabel: string) => Promise<void>;
    onPopoutClosed: (callback: (frameId: string, windowLabel: string) => void) => () => void;
    // Cross-window restore protocol (#1920):
    requestRestore: (windowLabel: string) => Promise<void>;
    onRestoreRequest: (callback: (windowLabel: string) => void) => () => void;
    sendRestorePayload: (windowLabel: string, propsJson: string) => Promise<void>;
    onRestorePayload: (callback: (windowLabel: string, propsJson: string) => void) => () => void;
    sendRestoreAck: (windowLabel: string) => Promise<void>;
    onRestoreAck: (callback: (windowLabel: string) => void) => () => void;
    sendReturnRequest: (windowLabel: string, propsJson: string) => Promise<void>;
    onReturnRequest: (callback: (windowLabel: string, propsJson: string) => void) => () => void;
  };

  inbox: {
    read: (draftNumber: number) => Promise<string | null>;
    write: (draftNumber: number, content: string) => Promise<boolean>;
    delete: (draftNumber: number) => Promise<InboxDeleteResult | null>;
    getActive: () => Promise<number>;
    setActive: (draftNumber: number) => Promise<boolean>;
    getCount: () => Promise<number>;
    new: () => Promise<number>;
    tidyUp: () => Promise<TidyUpResult | null>;
    reorder: (order: number[]) => Promise<TidyUpResult | null>;
    onChanged: (callback: () => void) => () => void;
    /** Compatibility controls: cloud mode resolves no-op; `dev:rest` uses local watcher endpoints. */
    watchActive: (draftNumber: number) => Promise<boolean>;
    /** Cloud changes arrive through the workspace drain rather than this watcher. */
    unwatchActive: () => Promise<boolean>;
    onExternalChange: (callback: (draftNumber: number) => void) => () => void;
  };

  frontmatter: {
    onSchemaChanged: (callback: () => void) => () => void;
  };

  workspace: {
    getDir: () => Promise<string>;
    setDir: () => Promise<string | null>;
    listAll: () => Promise<WorkspaceEntry[]>;
    register: (name: string, path: string) => Promise<WorkspaceEntry[]>;
    remove: (path: string) => Promise<WorkspaceEntry[]>;
    updateMetadata: (path: string, patch: WorkspaceMetadataPatch) => Promise<WorkspaceEntry[]>;
    switchTo: (path: string) => Promise<string>;
    listFiles: (path: string) => Promise<WorkspaceDirEntry[]>;
    scaffold: (options: WorkspaceScaffoldOptions) => Promise<WorkspaceEntry[]>;
    addExisting: (path: string) => Promise<WorkspaceEntry[]>;
  };

  dialog: {
    openDirectory: () => Promise<string | null>;
    openFile: () => Promise<string | null>;
    createDirectory: (path: string) => Promise<void>;
    openFiles: () => Promise<string[] | null>;
  };

  settings: {
    get: () => Promise<AppSettings | null>;
    save: (settings: AppSettings) => Promise<boolean>;
    // Quit-path blocking write (#2782): Tauri routes it to a blocking Rust
    // command; mock/REST fall back to a best-effort save.
    saveBlocking: (settings: AppSettings) => Promise<boolean>;
  };

  fonts: {
    list: () => Promise<Array<{ family: string; isMonospace: boolean }>>;
  };

  device: {
    getName: () => Promise<string | null>;
    setName: (name: string) => Promise<void>;
    clearName: () => Promise<void>;
  };

  auth: {
    getState: () => Promise<AuthState>;
    login: () => Promise<void>;
    logout: () => Promise<void>;
    onStateChanged: (callback: (state: AuthState) => void) => () => void;
  };

  subscription: {
    getInfo: () => Promise<SubscriptionInfo>;
    startTrial: () => Promise<SubscriptionInfo>;
    checkEntitlement: (feature: string) => Promise<boolean>;
    getPortalUrl: () => Promise<string>;
    onInfoChanged: (callback: (info: SubscriptionInfo) => void) => () => void;
  };

  cloudSync: {
    getStatus: () => Promise<CloudSyncStatusInfo>;
    triggerSync: () => Promise<CloudSyncStatusInfo>;
    connect: () => Promise<void>;
    disconnect: () => Promise<void>;
    setupEncryption: (password: string) => Promise<boolean>;
    verifyPassword: (password: string) => Promise<boolean>;
    armEncryption: (password: string) => Promise<ArmEncryptionResult>;
    getConflicts: () => Promise<CloudConflictItem[]>;
    resolveConflicts: (resolutions: Map<string, "use-local" | "use-server" | "save-both">) => Promise<void>;
    onStatusChanged: (callback: (status: CloudSyncStatusInfo) => void) => () => void;
    onRemoteChange: (callback: (changes: CloudConflictItem[]) => void) => () => void;
  };

  assets: {
    list: () => Promise<AssetEntry[]>;
    saveFile: (filename: string, data: string) => Promise<string>;
    importFile: (sourcePath: string) => Promise<string>;
    readFile: (filename: string) => Promise<string | null>;
    deleteFile: (filename: string) => Promise<boolean>;
  };

  publish: {
    getProfile: () => Promise<PublicProfile | null>;
    updateProfile: (data: { username: string; displayName: string; bio: string }) => Promise<PublicProfile>;
    deleteProfile: () => Promise<void>;
    publishPage: (data: { title: string; slug: string; contentMarkdown: string; description?: string }) => Promise<PublishedPage>;
    updatePage: (id: string, data: { title?: string; slug?: string; contentMarkdown?: string; description?: string }) => Promise<PublishedPage>;
    unpublishPage: (id: string) => Promise<void>;
    listPages: () => Promise<{ pages: PublishedPage[]; total: number }>;
    getPage: (id: string) => Promise<PublishedPage>;
    onPagesChanged: (callback: () => void) => () => void;
  };

  generator: {
    checkPath: (workspacePath: string) => Promise<PathClassification>;
    scaffold: (appName: string, workspacePath: string, preset?: string, settings?: Record<string, unknown>, force?: boolean) => Promise<GeneratorRunResult>;
    writeConfigs: (appName: string, workspacePath: string, force?: boolean) => Promise<GeneratorRunResult>;
    assembleChild: (options: GeneratorAssembleChildOptions) => Promise<GeneratorAssembleResult>;
    onAssembleProgress: (callback: (event: GeneratorAssembleProgressEvent) => void) => () => void;
    findLeafForWorkspace: (workspacePath: string) => Promise<LeafAppInfo | null>;
    openApp: (appPath: string) => Promise<void>;
  };

  window: {
    setOpacity: (opacity: number) => Promise<void>;
    print: () => Promise<void>;
    setTitle: (title: string) => Promise<void>;
  };

  similarDocs: {
    query: (draftContent: string, currentDraftNum: number | null, includePaths?: string[]) => Promise<SimilarDocResult[]>;
    getContent: (path: string) => Promise<string | null>;
    writeContent: (path: string, content: string) => Promise<boolean>;
    rebuildIndex: () => Promise<void>;
  };

  chat: (messages: ChatMessage[], options: ChatOptions) => AsyncIterable<ChatEvent>;

  inlineCommand: {
    stream: (input: InlineCommandStreamInput) => AsyncIterable<LlmChatEvent>;
  };

  fileSearch: FileSearchAPI;

  apiTokens: {
    list: () => Promise<ApiTokenMeta[]>;
    /** `input.scopes` omitted → `["full"]`; `input.workspaceId` omitted → unrestricted across the owner's workspaces. */
    create: (input: ApiTokenCreateInput) => Promise<ApiTokenMintResult>;
    revoke: (id: string) => Promise<void>;
  };

  getProjectRoot: () => Promise<string>;
  getHomeDir: () => Promise<string>;
  revealDirectory: (path: string) => Promise<void>;
}

Key Types

interface MessageMeta {
  filename: string;
  title: string;
  date: string | null;
  createdAt: string | null;
  updatedAt: string | null;
  sidebarPosition: number;
  modifiedAt: string;
  snippet?: string;
}

interface MessageWithBody extends MessageMeta {
  body: string;
}

interface PinEntry {
  name: string;
  type: "file" | "directory";
  path: string;
  title: string;
  description: string;
  modifiedAt: string;
  children?: PinEntry[];
}

interface InboxDeleteResult {
  newCount: number;
  newActive: number;
}

interface TidyUpResult {
  newCount: number;
  newActive: number;
}

interface WorkspaceEntry {
  path: string;
  name: string;
}

interface AssetEntry {
  filename: string;
  sizeBytes: number;
  modifiedAt: string;
}

type CloudSyncStatusType = "idle" | "syncing" | "synced" | "error" | "conflict" | "offline";

interface CloudSyncStatusInfo {
  status: CloudSyncStatusType;
  lastSyncedAt: string | null;
  filesCount: number;
  cursor: number;
  connectedDevices: number;
  error?: string;
}

interface CloudConflictItem {
  path: string;
  encryptedPath: string;
  localVersion: number;
  serverVersion: number;
  type: "concurrent-edit" | "edit-delete" | "delete-edit";
}

interface AuthUser {
  id: string;
  email: string;
  name: string;
  picture?: string;
}

interface AuthState {
  isAuthenticated: boolean;
  user: AuthUser | null;
}

Auth API

The auth domain manages user authentication state.

  • auth.getState() — Returns the current AuthState (authenticated flag and user info)

  • auth.login() — Starts the Better Auth browser handoff

  • auth.logout() — Clears tokens and reverts to unauthenticated state

  • auth.onStateChanged(callback) — Registers a listener for auth state changes; returns an unsubscribe function

const backend = getBackend();

// Check auth state
const auth = await backend.auth.getState();
if (auth.isAuthenticated) {
  console.log("Logged in as:", auth.user?.name);
}

// Login / logout
await backend.auth.login();
await backend.auth.logout();

// Listen for auth changes
const unsub = backend.auth.onStateChanged((state) => {
  console.log("Authenticated:", state.isAuthenticated);
});

Cloud Sync API

The cloudSync domain manages end-to-end encrypted cloud synchronization with real-time WebSocket updates.

  • cloudSync.getStatus() — Returns the current CloudSyncStatusInfo

  • cloudSync.triggerSync() — Initiates a manual push + pull sync

  • cloudSync.connect() — Connects the WebSocket for real-time change streaming

  • cloudSync.disconnect() — Disconnects the WebSocket

  • cloudSync.setupEncryption(password) — Sets up the encryption password for the workspace

  • cloudSync.verifyPassword(password) — Verifies an encryption password

  • cloudSync.armEncryption(password) — Arms E2E encryption; branches on workspace state (creates or verifies key)

  • cloudSync.getConflicts() — Returns pending CloudConflictItem[]

  • cloudSync.resolveConflicts(resolutions) — Resolves conflicts with a Map<path, strategy>

  • cloudSync.onStatusChanged(callback) — Listens for cloud sync status changes

  • cloudSync.onRemoteChange(callback) — Listens for real-time remote change notifications

const backend = getBackend();

// Check cloud sync state
const status = await backend.cloudSync.getStatus();
console.log(status.status); // "idle" | "syncing" | "synced" | "error" | "conflict" | "offline"

// Connect for real-time updates
await backend.cloudSync.connect();

// Listen for remote changes
const unsub = backend.cloudSync.onRemoteChange((changes) => {
  console.log("Remote changes:", changes.length);
});

// Trigger a manual sync
const updated = await backend.cloudSync.triggerSync();

// Handle conflicts
const conflicts = await backend.cloudSync.getConflicts();
const resolutions = new Map(conflicts.map((c) => [c.path, "use-server" as const]));
await backend.cloudSync.resolveConflicts(resolutions);

Assets API

The assets namespace manages media and binary files (images, etc.) stored in the workspace:

const backend = getBackend();

// List all assets
const assets = await backend.assets.list();
// [{ filename: "photo.png", sizeBytes: 12345, modifiedAt: "2026-03-24T10:00:00Z" }]

// Save a base64-encoded file
await backend.assets.saveFile("photo.png", base64Data);

// Import a file from the local filesystem
await backend.assets.importFile("/path/to/image.png");

// Read file content (returns base64)
const data = await backend.assets.readFile("photo.png");

// Delete
await backend.assets.deleteFile("photo.png");

Fonts API

The fonts namespace discovers installed system fonts:

const fonts = await backend.fonts.list();
// [{ family: "JetBrains Mono", isMonospace: true }, { family: "Arial", isMonospace: false }, ...]

Window API

await backend.window.setOpacity(0.8); // Set window transparency (0.3–1.0)
await backend.window.print();          // Print the current preview pane
await backend.window.setTitle("My App"); // Override the OS window title

Filesystem API

await backend.revealDirectory("/path/to/workspace");
// Opens the directory in the OS file manager (Finder on macOS)

TauriAdapter

createTauriAdapter() creates a BackendAPI that combines Tauri invoke() / listen() calls for surviving native operations with in-process access to the encrypted cloud workspace model for document domains. It is exported from the /tauri-adapter subpath (not the main package entry).

import { createTauriAdapter } from "@takazudo/backend-bridge/tauri-adapter";
const adapter = createTauriAdapter();

Event listeners use a syncListen helper that wraps Tauri's async listen() to return a synchronous unsubscribe function, matching the callback pattern used throughout the UI.

MockAdapter

createMockAdapter() creates an in-memory BackendAPI with a controls object for triggering events in tests.

import { createMockAdapter } from "@takazudo/backend-bridge";

const { api, controls } = createMockAdapter();

// Use in tests
initBackend(api);

// Trigger events
controls.triggerMessagesChanged("test.md");
controls.triggerNotesChanged("inbox");

// Access internal stores
controls.files.set("note.md", "# Hello");
controls.pinFiles.set("0:readme.md", "content");

MockControls

interface MockControls {
  triggerMessagesChanged: (filename?: string) => void;
  triggerPinFileChanged: (entryPath: string) => void;
  triggerDraftExternalChange: (draftNumber: number) => void;
  triggerIncomingWorkspaceConflict: (conflict: IncomingWorkspaceConflict) => void;
  triggerNotesChanged: (dir: string) => void;
  triggerSchemaChanged: () => void;
  setAuthState: (state: AuthState) => void;
  setSubscriptionInfo: (info: SubscriptionInfo) => void;
  triggerSubscriptionInfoChanged: (info: SubscriptionInfo) => void;
  addRemoteFile: (path: string, content: string) => void;
  files: Map<string, string>;
  pinFiles: Map<string, string>;
  remoteFiles: Map<string, string>;
  setPinTree: (pinIndex: number, entries: PinEntry[] | null) => void;
  setPinChildren: (pinIndex: number, dirPath: string, entries: PinEntry[] | null) => void;
  simulateAuthFailure: (errorMessage?: string) => void;
  simulateEncryptionKeyMismatch: (errorMessage?: string) => void;
  simulateNetworkDownFirstSync: (errorMessage?: string) => void;
  simulateCorruptedLocalSandbox: (errorMessage?: string) => void;
  iosDegradedState: { type: string; errorMessage?: string } | null;
  seedScaffoldTarget: (path: string, state: { hasSettings?: boolean; visibleChildren?: number }) => void;
  setFindLeafResult: (result: LeafAppInfo | null) => void;
}

RestAdapter

createRestAdapter() creates a BackendAPI that communicates with the Rust backend over HTTP/REST endpoints on localhost:3001 with Server-Sent Events (SSE) for real-time updates.

import { createRestAdapter } from "@takazudo/backend-bridge";

const adapter = createRestAdapter(); // defaults to http://localhost:3001
// or
const adapter = createRestAdapter("http://localhost:4000");

The REST adapter maps each BackendAPI method to an HTTP endpoint (e.g., GET /api/messages, POST /api/notes/inbox/new). Real-time events use a shared EventSource connection with reference counting.

Default Exports

The package exports default values for cloud sync and auth state:

import { defaultCloudSyncStatus, defaultAuthState } from "@takazudo/backend-bridge";

// defaultCloudSyncStatus: { status: "idle", lastSyncedAt: null, filesCount: 0, cursor: 0, connectedDevices: 0 }
// defaultAuthState: { isAuthenticated: false, user: null }

Dependencies

  • @takazudo/app-defaults — settings types

  • @takazudo/cloud-crypto — encryption key derivation for cloud sync

  • @takazudo/cloud-sync — cloud sync client (WebSocket manager, change tracker)

  • @takazudo/file-utils — frontmatter parsing (used by MockAdapter)

  • @takazudo/inline-command-skills — skill loader types (used by files namespace)

Tauri packages (@tauri-apps/api, @tauri-apps/plugin-dialog, @tauri-apps/plugin-shell) are peer dependencies — optional, required only when using the TauriAdapter in a real Tauri app.