Murasaki
ガイド

Node Main

長寿命 Node lifecycle と 'use main' による型付き renderer-to-Node 関数。

Node Main はデスクトップアプリと同じ期間生存する処理に使います。DB connection、 filesystem watcher、socket、worker pool、background queue、ネイティブホスト終了前に 完了すべき cleanup が対象です。

ライフサイクルを定義する

src/main.ts を作り、defineMain() を default export します。

src/main.ts
import { defineMain } from 'murasaki/main'
import { watch, type FSWatcher } from 'node:fs'

let watcher: FSWatcher | undefined

export default defineMain({
  async ready({ paths, signal, isPackaged }) {
    watcher = watch(paths.data, { recursive: true }, (_event, filename) => {
      console.log('changed', filename)
    })

    signal.addEventListener('abort', () => watcher?.close(), { once: true })
    console.log(isPackaged ? 'packaged main ready' : 'development main ready')
  },

  async beforeQuit({ reason }) {
    // 通常の close を本当に止める必要がある場合だけ false を返す。
    console.log('quit requested:', reason)
  },

  async secondInstance(_context, { argv, cwd }) {
    // packaged macOS / Windows: 別 launch が primary に転送された。
    console.log('second launch:', { argv, cwd })
  },

  async openRequested(_context, event) {
    // 登録済み URL scheme / file type は ready() の完了後に届く。
    console.log('open targets:', event.targets)
  },

  async shutdown() {
    watcher?.close()
    watcher = undefined
  },
})

既定で src/main.ts を検出します。別 entry や cleanup 上限は設定できます。

murasaki.config.ts
export default defineConfig({
  appId: 'com.example.notes',
  productName: 'Notes',
  main: {
    entry: 'src/backend/main.ts',
    shutdownTimeoutMs: 15_000,
  },
})

src/main.ts があっても無効化する場合は main: false にします。

Context reference

Field意味
appId, productName, version解決済みアプリ identity
isPackagedmurasaki devfalse、bundle 内は true
platform, arch実行 target (process.platform / process.arch)
projectRootdev は project root、本番は package resource の working directory
resourcesPathread-only のアプリ resource location
paths.data永続アプリデータ
paths.cache再生成可能な cache
paths.logslog file
paths.temp一時 staging data
signalbeforeQuit 後、shutdown 前に abort

終了理由は window-closeapp-quitsignalrestartdev-reloadstartup-failure です。順序とキャンセルは プロセスモデルを参照してください。

secondInstance(context, event)

packaged macOS / Windows app は per-user / per-appId lock を使います。別 launch が primary app に転送されると、Murasaki は window を focus して次の値で secondInstance() を呼びます。

フィールド意味
event.argv2 回目の launcher に渡された argument。caller が渡した URL / file argument を含む
event.cwd2 回目の launch の working directory

使用前にすべての argument を検証してください。secondInstance() は low-level の process activation hook です。登録済み URL scheme / file type と一致するかに関係なく、 raw launch argument を受け取ります。murasaki dev と Linux では現状この hook は 配信されません。

openRequested(context, event)

設定済み URL scheme / file association には openRequested() を使います。cold-start argv、second-instance argv、native URL / file event を型付き URL / file target に 正規化し、ready() の完了後にだけ実行します。

認識済み target を伴う2回目の launch では、secondInstance()openRequested() の両方が実行されます。同じ item を両方で開かないでください。 一般的な activation behavior は secondInstance()、URL / file の意味的な処理は openRequested() に置きます。

event shape、設定、packaging behavior、security requirement は Deep Link と File Associationを参照してください。

'use main' で renderer から Node を呼ぶ

top-level の 'use main' directive は export 関数を src/main.ts と同じ Node module graph 上の型付き呼び出しとして公開します。

src/backend/checksum.ts
'use main'

import { createHash } from 'node:crypto'

export async function sha256(text: string): Promise<string> {
  if (typeof text !== 'string' || text.length > 1_000_000) {
    throw new TypeError('text must be a string no larger than 1 MB')
  }
  return createHash('sha256').update(text).digest('hex')
}

client component から通常どおり import します。renderer bundle には fetch stub だけが 入り、Node 実装は WebView に配信されません。

src/app/page.tsx
'use client'

import { useState } from 'react'
import { sha256 } from '../backend/checksum'

export default function Page() {
  const [digest, setDigest] = useState('')

  return (
    <button onClick={() => void sha256('Murasaki').then(setDigest)}>
      Hash {digest && `(${digest.slice(0, 8)}…)`}
    </button>
  )
}

Main event を renderer へストリームする

接続状態、進捗、device event、watcher出力など、長寿命Node処理から発生する値には 型付きevent channelを使います。

src/main.ts
import { defineMain, emitMainEvent } from 'murasaki/main'

export default defineMain({
  async ready({ signal }) {
    const timer = setInterval(() => {
      emitMainEvent('relay.status', { connected: true, at: new Date() })
    }, 1_000)
    signal.addEventListener('abort', () => clearInterval(timer), { once: true })
  },
})

client componentで購読します。

'use client'

import { useEffect, useState } from 'react'
import { subscribeMainEvent } from 'murasaki/main-client'

export function RelayStatus() {
  const [connected, setConnected] = useState(false)
  useEffect(() => subscribeMainEvent<{ connected: boolean; at: Date }>(
    'relay.status',
    (event) => setConnected(event.connected),
  ), [])
  return <output>{connected ? 'Connected' : 'Disconnected'}</output>
}

eventは'use main'と同じrich-value wire codecを使い、認証済みapp-local SSEで届きます。 一時切断後はEventSourceが再接続しますが、eventはlive-onlyでreplay bufferやdurable queueは ありません。取りこぼしを復元する必要がある場合、永続状態をNodeに保存し、'use main' のsnapshot関数も公開してください。

'use main' は experimental な RPC 境界であり、認可境界ではありません。すべての 引数を信頼できない renderer input として扱い、型検証、filesystem path の正規化、 app data へのアクセス認可を Node 関数内で実施してください。

Wire value と上限

version 付き wire codec は primitive、undefinedbigintDateMapSet、 循環する plain object / array、ArrayBuffer、typed array、BlobFileFormData、構造化 Error を扱えます。

  • request / response の最大サイズ: 32 MiB
  • function、symbol、custom prototype を持つ instance は拒否
  • call自体はrequest/response。live pushにはMain eventを使います。'use main' callから AsyncIterableを直接返すことは未実装
  • named function / async function / const / let / var を export 可能。 実行時に選択された export は callable である必要があります

大きなファイルの bytes を RPC に通さないでください。検証済みの app-owned id / path だけを渡し、Node 内で完結して stream します。

'use main' / 'use server' / API route

選択用途
'use main'長寿命 app backend への型付き imperative call
'use server'ActionState を使う React action / form 形式の mutation
src/api/**/route.tsRequest/Response、streaming body、HTTP method routing

いずれも app-local Node runtime で実行されます。API route は public network server では なく、内部 RPC と同じ app runtime session で保護されます。

現在の制限

Node Mainにはimperativeなnative window生成APIがありません。宣言windowはtrusted renderer からmurasaki/nativeで操作します。tray menuはそのrenderer APIから利用できますが、 Node Main自体に直接のtray menu / global shortcut APIはまだありません。 packaged macOS / Windows の single-instance delivery は secondInstance()、設定済み URL scheme / file association は openRequested() で利用できます。Windows portable archive、Linux、murasaki dev は system association metadata を install しません。 renderer-safe な dialog、clipboard、notification、shell、 基本 window command は capabilities allowlist の下で別途 murasaki/native から利用 できます。computed import、dynamic native addon、runtime-discovered asset を使う Node package は明示的な bundle.external / bundle.resources が必要な場合があります。 murasaki dev だけでなく clean machine で installed artifact を必ずテストしてください。

次へ

On this page