Nuvaka › Developer docs › Examples

Example extensions

The five examples live under generalappclient/examples/apps/ in the app's source repository. All of them pass nuvaka-ext lint with no errors and no warnings. Their UI texts and code comments are in Turkish.

ExampleShowsPermissions
Hellotwo pages, background, notification, local clipboard, network, MVCC counternotifications, background, clipboard: write, net
Notes summaryreading notes, a summary per key, storage.updatenuvaka.notes: read
Clipboard historyclipboard pool, schedule, onChanged, versioned deletenuvaka.clipboard: read, background
GitHub watchernet, push/notify, notification limit, one notification via MVCCnet, nuvaka.push, notifications, background
Local file editorpicking folders, listing, read/write, inline confirmationfiles: read, files: write
nuvaka-ext lint examples/apps/github-watcher
nuvaka-ext pack examples/apps/github-watcher --out /tmp/

When loaded from a folder, local capabilities work; server endpoints such as storage, notes, the clipboard pool and push need the extension to be installed on your account (quick start).

Hello (tarikgkhsn.hello)

The smallest complete example: two pages (main, about) with nuvaka.ui.onPage, a local notification, writing to the local clipboard, a request to api.github.com, a request to a host that is not allowed (rejected) and a counter with storage.update. The background fires on every 15m, app.started and app.focused.

document.getElementById('fetch').onclick = run(async () => {
  const r = await nuvaka.net.fetch('https://api.github.com/repos/tauri-apps/tauri', { headers: { Accept: 'application/vnd.github+json' } })
  const j = await r.json()
  return { status: r.status, name: j.full_name, stars: j.stargazers_count }
})
// MVCC: update() okur, yazar; başka cihaz araya girdiyse güncel değerle yeniden dener
document.getElementById('count').onclick = run(async () => {
  const r = await nuvaka.storage.update('count', (n) => (n || 0) + 1)
  return `sayaç: ${r.value} (sürüm ${r.version})`
})
nuvaka.storage.onChanged((key, e) => { if (key === 'count') log(`başka yerde değişti: ${key} = ${e.value} (sürüm ${e.version})`) })
document.getElementById('blocked').onclick = run(() => nuvaka.net.fetch('https://example.com/'))

Source: examples/apps/hello/app.js (lines 21–32)

// Nuvaka Apps örneği (arka plan, QuickJS). Tek dosyalık ES modülü; `nuvaka` hazır gelir.
let ticks = 0

nuvaka.on('app.started', () => console.log('arka plan: uygulama açıldı'))
nuvaka.on('app.focused', () => { ticks++ })

nuvaka.on('schedule', async ({ schedule, at }) => {
  const settings = await nuvaka.settings.get()
  console.log(`zamanlanmış görev: ${schedule} @ ${new Date(at * 1000).toISOString()} (odak ${ticks} kez)`)
  await nuvaka.ui.notify(settings.greeting, `Zamanlanmış görev çalıştı (${schedule})`)
})

setTimeout(() => console.log('arka plan: 2 sn sonra setTimeout çalıştı'), 2000)

Source: examples/apps/hello/background.js

Notes summary (tarikgkhsn.notes-summary)

Lists notes and builds a short on-device summary and keywords for the selected note (no external service or AI). The summary lives under the summary/<note id> key; if the note did not change, the update function returns undefined and nothing is written. A summary written by another device reaches the list through onChanged.

/** Özeti depodan okur; yoksa ya da not değiştiyse çıkarıp yazar. update() çakışmada güncel değerle yeniden dener. */
async function ensureSummary(note) {
  const stamp = stampOf(note)
  let wrote = false
  const r = await nuvaka.storage.update(keyOf(note.id), (cur) => {
    if (cur && cur.stamp === stamp && cur.n === perSummary) return undefined // güncel: yazma yok
    const s = Summarize.summarize(note.content, perSummary)
    wrote = true
    return { stamp, n: perSummary, sentences: s.sentences, keywords: s.keywords, at: new Date().toISOString() }
  })
  if (r && r.value) cache.set(note.id, r.value)
  return { value: r && r.value, wrote }
}

Source: examples/apps/notes-summary/app.js (lines 43–55)

The notes.get(id) response is { note, images, reminders }; summarizing is a pure function (summarize.js): it scores sentences by word frequency and returns the best N in their original order. The sentence count comes from the sentences setting in the manifest's settings.

Clipboard history (tarikgkhsn.clipboard-history)

Every 15 minutes and at start, the background reads the last 50 items of the clipboard pool and adds new texts to a single history key. Even if two devices' backgrounds run at the same time, nothing is lost because update retries with the current value on conflict. Nothing is written when there is nothing new. background.js is also loaded in the UI; the "Şimdi al" (take now) button calls the same function, and event listeners are only registered when nuvaka.background is true.

/** Havuzdaki son metinleri geçmişe ekler; eklenen sayısını döner. */
async function snapshot() {
  const settings = await nuvaka.settings.get()
  const keep = Number(settings.keep) || 200
  // → { items: [{ id, deviceId, deviceName, kind, mime, fileName, size, hash, createdAt, text, textTruncated, blocks }] } (yeniden eskiye)
  const r = await nuvaka.pool.list({ limit: 50 })
  const items = (r && r.items) || []
  const fresh = items
    .filter((i) => i && typeof i.text === 'string' && i.text.trim() && (!i.kind || i.kind === 'text'))
    .map((i) => ({ id: i.id, text: cut(i.text), device: i.deviceName || null, at: i.createdAt || null }))
    .sort((a, b) => b.id - a.id)

  let added = 0
  // MVCC: update() okur, fn'i çağırır, okunan sürümle yazar; başka cihaz araya girdiyse güncel değerle yeniden dener
  await nuvaka.storage.update(HISTORY_KEY, (cur) => {
    const list = (cur && Array.isArray(cur.items)) ? cur.items : []
    const lastId = (cur && cur.lastId) || 0
    // Yalnız son görülen kimlikten yeniler; aynı metin bir kez (havuz en yeniden eskiye gelir, en yenisi kalır)
    const add = [], seen = new Set()
    for (const e of fresh) if (e.id > lastId && !seen.has(e.text)) { add.push(e); seen.add(e.text) }
    added = add.length
    if (added === 0) return undefined // değişiklik yok: yazma, sürüm artmaz
    const merged = add.concat(list.filter((e) => !seen.has(e.text))).slice(0, keep)
    return { items: merged, lastId: Math.max(lastId, ...add.map((e) => e.id)) }
  })
  return added
}

if (nuvaka.background) {
  const run = (why) => snapshot()
    .then((n) => console.log(`pano geçmişi (${why}): ${n} yeni kayıt`))
    .catch((e) => console.error(`pano geçmişi (${why}) başarısız: ${e.code} ${e.message}`))
  nuvaka.on('schedule', (ev) => run(ev.schedule))
  nuvaka.on('app.started', () => run('açılış'))
}

Source: examples/apps/clipboard-history/background.js (lines 11–45)

The UI receives the background's writes through onChanged; "Geçmişi temizle" (clear history) calls storage.delete with the version read, and gets version_conflict if the background wrote in the meantime:

// Arka plan ya da başka cihaz yazdı: olay yalnız bildiğimizden yeni sürümle gelir; değer olayla birlikte okunmuş olur
nuvaka.storage.onChanged((key, c) => {
  if (key === null) { hist = { value: null, version: 0 }; render(); return }
  if (key !== HISTORY_KEY) return
  hist = c.deleted ? { value: null, version: 0 } : { value: c.value, version: c.version }
  render()
})

Source: examples/apps/clipboard-history/app.js (lines 35–41)

GitHub watcher (tarikgkhsn.github-watcher)

Every hour (and when the network comes back) it reads the latest release of each listed repository from api.github.com. On a new release it sends nuvaka.push (all devices) or nuvaka.ui.notify (this device), as the user chose. The repository list is edited in the UI and kept across devices under the config key; without it, the manifest settings defaults are used.

If the notification limit is reached (notify_limit) the error is caught, no further notifications are tried in that round and the record stays "notification pending"; it is retried at the next check:

async function announce(channel, title, body) {
  if (channel === 'none') return 'off'
  try {
    if (channel === 'local') await nuvaka.ui.notify(title, body) // yalnız bu cihaz
    else await nuvaka.push(title, body) // tüm cihazlar
    return 'sent'
  } catch (e) {
    // Yerel: notify_limit. Push: kullanıcı "Hayır" dediyse sunucu notify_limit döner. İkisi de sayaç olarak ortaktır.
    if (e.code === 'notify_limit' || e.code === 'notify_limit_reached') return 'limited'
    throw e
  }
}

Source: examples/apps/github-watcher/background.js (lines 44–55)

Because every device runs its own schedule, two devices can see the same new release at the same time. Before notifying, the record is set to notified: true with update; only one device succeeds, the other sees the current value and backs off:

  const key = releaseKey(repo)
  const now = new Date().toISOString()
  // 1) Kaydı güncelle: etiket değiştiyse yeni kayıt, notified=false (ilk görüşte true: eski sürümü duyurma)
  const saved = await nuvaka.storage.update(key, (cur) => {
    const tag = rel ? rel.tag : null
    if (cur && cur.tag === tag) return undefined // değişiklik yok, yazma yok
    return { repo, tag, name: rel && rel.name, url: rel && rel.url, at: rel && rel.at, seenAt: now, notified: !cur || !tag }
  })
  results.push({ repo, tag: rel ? rel.tag : null })
  const v = saved && saved.value
  if (!v || v.notified || limited) continue

  // 2) Bildirimi sahiplen: MVCC sayesinde iki cihaz aynı anda denetlese de yalnız biri notified=true yazabilir
  let mine = false
  await nuvaka.storage.update(key, (cur) => {
    if (!cur || cur.tag !== v.tag || cur.notified) return undefined
    mine = true
    return Object.assign({}, cur, { notified: true })
  })
  if (!mine) continue
  const res = await announce(cfg.channel, `${repo} ${v.tag}`, v.name && v.name !== v.tag ? v.name : 'Yeni sürüm yayınlandı')
  if (res === 'limited') {
    // Sınır doldu: kaydı geri al, bir sonraki denetimde yeniden denenir; bu turda başka bildirim deneme
    limited = true
    await nuvaka.storage.update(key, (cur) => (cur && cur.tag === v.tag ? Object.assign({}, cur, { notified: false }) : undefined))
  }
}

Source: examples/apps/github-watcher/background.js (lines 69–95)

Local file editor (tarikgkhsn.local-editor)

Lists files in folders the user picked with the Klasör ekle (add folder) button, opens, edits and saves text files, and creates and deletes files and folders. The pick is per device (files.folders()). Non-text files open read-only. Because frames have no confirm(), questions are asked inside the page. Local files have no version, so size and modification time are read again before saving:

async function save() {
  if (!file || file.readOnly || !isDirty()) return
  try {
    // Başka bir program bu arada değiştirdiyse sor (yerel dosyada sürüm yok; boyut + değişme zamanı karşılaştırılır)
    const st = await statOf(file.path)
    if (st && (st.mtime !== file.mtime || st.size !== file.size) && !(await ask('Dosya diskte değişmiş. Üzerine yazılsın mı?', 'Üzerine yaz'))) return
    const text = $('editor').value
    await nuvaka.files.writeText(root, file.path, text)
    original = text
    const after = await statOf(file.path)
    if (after) { file.size = after.size; file.mtime = after.mtime }
    setStatus(`Kaydedildi · ${new Date().toLocaleTimeString()}`)
    syncButtons()
    if (parentOf(file.path) === dir) openDir(dir)
  } catch (e) { setStatus(errText(e), true) }
}

Source: examples/apps/local-editor/app.js (lines 121–136)

$('pick').onclick = async () => {
  try {
    const picked = await nuvaka.files.pick() // kullanıcı vazgeçerse null
    if (picked) await loadRoots(picked)
  } catch (e) { setStatus(errText(e), true) }
}

Source: examples/apps/local-editor/app.js (lines 183–188)

Nuvaka Apps API v1 · last updated 2026-09-27