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.
| Example | Shows | Permissions |
|---|---|---|
| Hello | two pages, background, notification, local clipboard, network, MVCC counter | notifications, background, clipboard: write, net |
| Notes summary | reading notes, a summary per key, storage.update | nuvaka.notes: read |
| Clipboard history | clipboard pool, schedule, onChanged, versioned delete | nuvaka.clipboard: read, background |
| GitHub watcher | net, push/notify, notification limit, one notification via MVCC | net, nuvaka.push, notifications, background |
| Local file editor | picking folders, listing, read/write, inline confirmation | files: 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