Nuvaka › Geliştirici belgeleri › Gerçek zamanlı odalar
Gerçek zamanlı odalar: nuvaka.realtime
Kullanıcılar arasında anlık veri alışverişi: online oyunlar, ortak tahta, birlikte düzenleme. Bir kullanıcı oda kurar, diğerleri 6 karakterlik kodla ya da arkadaş davetiyle katılır; odadakiler birbirine mesaj gönderir ve sürümlü bir ortak durumu paylaşır. Sunucu yalnız aktarır, mesaj saklamaz.
Sürüm: Nuvaka 0.3.1 ve geliştirici paketinde SDK 1.2.0 ile gelir. Manifestte "minAppVersion": "0.3.1" yaz.
Oda mı, uzantı sunucusu mu?
| Gerçek zamanlı oda | Uzantı sunucusu | |
|---|---|---|
| Kod | Yok; kararları istemciler verir, Nuvaka yalnız aktarır | Paketindeki sunucu kodu Nuvaka'nın makinesinde çalışır, kararı o verir |
| Ömür | Bellekte; boş kalınca ya da en geç 24 saatte kapanır | Yayıncı sunucuları sürekli açık; kullanıcı sunucuları en çok 12 saat |
| Kalıcı veri | Yok (paylaşılan durum oda kapanınca gider) | Uzantı başına SQLite (nuvaka.db) |
| İnceleme | Normal kurallar | server olan her sürüm incelenir |
| Uygun | Arkadaşlarla küçük oyunlar, ortak tahta, birlikte düzenleme | 7/24 açık dünyalar, hile yapılamayan oyunlar, çok oyunculu lobiler |
İki özellik de aynı izni (nuvaka.realtime) ve uygulamanın aynı gerçek zamanlı bağlantısını kullanır.
İzin
"permissions": [
{ "nuvaka.realtime": true, "reason": { "tr": "Arkadaşlarınla aynı odada oynamak için", "en": "To play with your friends in the same room" } }
]
- Değer yalnız
trueolabilir. Risk orta, denetim sunucuda. Kullanıcının gördüğü: "Seçtiğin kişilerle anlık veri alışverişi yapar (oyun odası, ortak çalışma)". - Okuma izinlerinden biriyle (
files.read,nuvaka.cloud.read,nuvaka.notes.read…) birlikte istenirse kurulumda sızıntı uyarısı çıkar: "… ve bunları odadaki diğer kişilere gönderebilir". - Kodda
nuvaka.realtime(ya danuvaka.servers) geçip izin istenmezsenuvaka-ext lintve yükleme reddeder. - Odaları yalnız uygulamanın kendisi kullanabilir: başka bir uzantının gizli kopyası (
usesbağımlılığı) olarak çalışırken çağrılar403 not_for_copiesdöner. Oda tek bir uzantıya aittir; başka uzantı o odayı göremez. - Güvenli oturumda (Güvenli Alan dosyası açıkken) kapalıdır:
secure_session.
Nerede çalışır
| Çağrılar | Arayüz | Arka plan |
|---|---|---|
create, join, get, invite, setState, update, leave | ✓ | ✓ |
connect, send, sendTo | ✓ | ui_only |
Bağlantıyı uygulamanın kabuğu tutar. Arayüzde create ve join odaya kendiliğinden bağlanır; olaylar gelmeye başlar. Arka planda oda kurulur ya da durum yazılır ama mesaj alınıp gönderilmez.
Oda kurmak
const room = await nuvaka.realtime.create({ maxMembers: 2, joinByCode: true, meta: { game: 'xox' } })
showCode(room.code) // ör. "K7Q2MX"; yalnız kurucuya görünür
| Seçenek | Kural |
|---|---|
maxMembers | 2–16, varsayılan 8 |
joinByCode | kodla katılma açık mı; varsayılan true. false ise yalnız davetle girilir. |
meta | odaya iliştirilen küçük bilgi, en çok 1 KB JSON |
Kişi başına aynı anda en çok 5 oda olabilir; fazlası too_many_rooms (429). Çağrılar bir oda görünümü döner:
| Alan | Açıklama |
|---|---|
roomId | odanın kimliği |
extensionId | odanın ait olduğu uzantı |
maxMembers, meta | kurulurken verilenler |
code | 6 karakterlik katılma kodu (büyük harf ve rakam); yalnız kurucuda dolu, diğerlerinde null |
isCreator | odayı sen mi kurdun |
members | [{ username, displayName, online }] |
state, stateVersion | paylaşılan durum ve sürümü; yeni odada null ve 0 |
createdAt | kuruluş zamanı (ISO) |
nuvaka.realtime.get(roomId) güncel görünümü okur. Yalnız üye ya da davetli görebilir (not_member); oda yoksa room_not_found.
Kodla katılmak
const room = await nuvaka.realtime.join('k7q2mx') // büyük/küçük harf fark etmez
Kod yalnız bu uzantının odası için geçerlidir. Geçersiz kod ya da kapanmış oda room_not_found, dolu oda room_full (409) verir. Oda kapanınca kod da geçersiz olur.
Arkadaş davet etmek
await nuvaka.realtime.invite(room.roomId, 'veli')
- Yalnız arkadaşlar davet edilebilir (
not_friend, 403). Arkadaşında uygulaman kurulu olmalı (not_installed, 409). Davet eden odanın üyesi ya da davetlisi olmalı (not_member). - Bir odada davetli ve üyelerin toplamı
maxMembers'ın iki katını geçemez (too_many_invites). - Davet edilen, kendi dilinde (Türkçe/İngilizce) bir
ext_realtime_invitebildirimi alır. Bildirimin bağlantısı/apps?id=<uzantı>&room=<roomId>biçimindedir: tıklayınca uygulaman açılır ve bağlamdactx.realtimeRoomgelir. - Davetli üye olmadan önce
connectile girer. Uygulaman zaten açıkken davete tıklanırsaonInviteçalışır. - Arkadaş listesini kendi arayüzünde göstermek istersen ayrıca
nuvaka.friendsokuma izni gerekir;inviteiçin gerekmez.
nuvaka.ready.then(async () => {
const id = nuvaka.realtime.invitedRoom() // ctx.realtimeRoom; davetle açılmadıysa null
if (id) enter(await nuvaka.realtime.connect(id))
})
nuvaka.realtime.onInvite(async ({ roomId }) => enter(await nuvaka.realtime.connect(roomId)))
Üyeler
Odadakiler birbirinin yalnız kullanıcı adını ve görünen adını görür; ikisi de sunucudan gelir. Profili gizli olanın displayName değeri null'dır. E-posta ve kullanıcı kimliği paylaşılmaz.
nuvaka.realtime.onMember(({ roomId, type, username, displayName }) => {
// type: 'joined' (katıldı) | 'offline' (bağlantısı koptu, üyeliği sürer) | 'left' (ayrıldı)
})
Mesajlar
await nuvaka.realtime.send(roomId, { t: 'emoji', v: '👍' }) // odadaki diğerlerine
await nuvaka.realtime.sendTo(roomId, 'veli', { t: 'hint', v: 3 }) // tek kişiye
nuvaka.realtime.onMessage(({ roomId, from, data, to, ts }) => {
// from: { username, displayName }; to: tek kişiye gönderildiyse kullanıcı adı, değilse null; ts: ms
})
dataherhangi bir JSON değeri olabilir, en çok 16 KB (message_too_large).- Kişi başına bir odada saniyede 20 mesaj gönderilebilir, kısa süreli 40'a kadar patlama serbesttir; aşımda
rate_limited. Hareket gibi sık güncellemeleri birleştirip seyrek gönder. - Göndermek için odaya bağlı olmalısın (
not_joined,not_connected);sendTohedefi odada değilsemember_not_found. - Mesaj içerikleri sunucuda saklanmaz ve loglanmaz. Sonradan katılan eski mesajları göremez: geç katılanın bilmesi gerekeni paylaşılan duruma koy.
Paylaşılan durum ve ifVersion
Her odanın tek bir paylaşılan durumu vardır (JSON, en çok 64 KB; aşarsa state_too_large). Durum depolamadaki gibi MVCC ile yazılır: her yazma okuduğun stateVersion'ı taşır, sunucudaki sürüm değiştiyse yazma reddedilir. Böylece iki oyuncu aynı anda hamle yaparsa biri diğerinin hamlesini ezmez.
// Doğrudan: okunan sürümle yaz
const room = await nuvaka.realtime.get(roomId)
try {
const { stateVersion } = await nuvaka.realtime.setState(roomId, next, { ifVersion: room.stateVersion })
} catch (e) {
if (e instanceof VersionConflictError) {
// e.current: güncel stateVersion, e.value: güncel durum → yeniden hesapla
}
}
// Kolay yol: oku → değiştir → yaz; çakışınca tazesiyle yeniden dener (varsayılan 5 deneme)
await nuvaka.realtime.update(roomId, (state, room) => {
if (!state || state.turn !== mine) return undefined // undefined: hiçbir şey yazma
return { ...state, turn: other }
})
ifVersionzorunludur: verilmezseif_version_required(428). Yeni odada sürüm0'dır, her başarılı yazma bir artırır.- Durumu yalnız odanın üyeleri yazabilir (
not_member). - Her değişiklik odaya bağlı herkese, yazan dahil,
realtime.stateolayıyla gider:{ roomId, stateVersion, state, by }. Arayüzü bu olaydan çiz; yazma sonucunu ayrıca çizmene gerek yok. updateişlevi aynı çağrıda birkaç kez çalışabilir; yan etkisiz tut.
nuvaka.realtime.onState(({ roomId, stateVersion, state, by }) => render(state))
Ayrılmak ve odanın kapanması
nuvaka.realtime.leave(roomId)→{ success, closed }. Kurucu ayrılırsa ya da oda boşalırsa oda kapanır ve kod geçersiz olur.- Odalar sunucunun belleğinde tutulur. Kimse bağlı değilken 30 dakika sonra, her durumda en geç 24 saatte kapanır. Sunucu yeniden başlarsa odalar kapanır.
- Kapanınca bağlı herkese
realtime.closed { roomId }gider. Bağlantı dönünce oda bulunamazsa olayreasontaşır (room_not_found,not_member,room_full). room_not_foundalınca yeni oda kur; eski odayı geri getirmeye çalışma.
Bağlantı durumu
nuvaka.realtime.onStatus(({ connected, reconnecting, error }) => {
banner.textContent = connected ? '' : reconnecting ? 'Yeniden bağlanıyor…' : 'Bağlantı yok'
})
Bağlantı koparsa kabuk yeniden bağlanır ve odalara kendiliğinden yeniden girer; bu sırada reconnecting: true gelir. Aynı bağlantı uzantı sunucuları için de kullanılır.
Olaylar
Kısayollar nuvaka.realtime.onX(fn) dinlemeyi kaldıran bir işlev döner; nuvaka.on('realtime.message', fn) ile de dinlenebilir. Olaylar yalnız arayüze, yalnız bağlı olduğun odalar için gelir.
| Olay | Kısayol | Yük |
|---|---|---|
realtime.message | onMessage | { roomId, from: { username, displayName }, data, to, ts } |
realtime.member | onMember | { roomId, type: 'joined' | 'offline' | 'left', username, displayName? } |
realtime.state | onState | { roomId, stateVersion, state, by } |
realtime.closed | onClosed | { roomId, reason? } |
realtime.status | onStatus | { connected, reconnecting?, error?: { code, message } } |
realtime.invite | onInvite | { roomId } — uygulama açıkken davet bağlantısına tıklandı |
Çağrılar
| Çağrı | Dönüş |
|---|---|
create({ maxMembers?, joinByCode?, meta? }) | oda görünümü |
join(code) | oda görünümü |
get(roomId) | oda görünümü |
invite(roomId, username) | { success: true } |
setState(roomId, state, { ifVersion }) | { success: true, stateVersion } |
update(roomId, fn, tries?) | { stateVersion, state } |
leave(roomId) | { success: true, closed } |
connect(roomId) | oda görünümü; yalnız arayüz |
send(roomId, data), sendTo(roomId, username, data) | null; yalnız arayüz |
invitedRoom() | davet bağlantısıyla açıldıysa oda kimliği, değilse null |
Hata kodları
| Kod | Anlamı |
|---|---|
permission_required | nuvaka.realtime izni yok |
not_for_copies | (403) gizli kopya olarak çalışan uzantı oda kullanamaz |
secure_session | güvenli oturumda kapalı |
room_not_found | (404) oda yok, kapandı ya da kod geçersiz |
not_member | (403) odanın üyesi ya da davetlisi değilsin |
room_full | (409) oda maxMembers sınırında |
too_many_rooms | (429) aynı anda en çok 5 oda |
not_friend | (403) yalnız arkadaş davet edilebilir |
not_installed | (409) arkadaşında uygulama kurulu değil |
too_many_invites | (409) davetli + üye sayısı maxMembers'ın iki katında |
if_version_required | (428) setState sürümsüz çağrıldı |
version_conflict | (409) okunan sürüm eskidi; VersionConflictError, e.current güncel sürüm, e.value güncel durum |
state_too_large | (413) durum 64 KB'ı aşıyor |
message_too_large | mesaj 16 KB'ı aşıyor |
rate_limited | saniyede 20 mesaj sınırı (40 patlama) aşıldı |
not_joined, not_connected | odaya ya da gerçek zamanlı bağlantıya bağlı değilsin |
member_not_found | sendTo hedefi odada değil |
ui_only | connect/send/sendTo arka planda çağrıldı |
Sunucu uçları (başvuru)
SDK çağrıları uygulamanın uzantı token'ıyla şu uçlara gider; normalde doğrudan çağırmazsın.
| Uç | SDK |
|---|---|
POST /api/ext/v1/x/realtime/rooms | create |
GET /api/ext/v1/x/realtime/rooms/{id} | get |
POST /api/ext/v1/x/realtime/join { code } | join |
POST /api/ext/v1/x/realtime/rooms/{id}/invite { username } | invite |
PUT /api/ext/v1/x/realtime/rooms/{id}/state { state, ifVersion } | setState, update |
POST /api/ext/v1/x/realtime/rooms/{id}/leave | leave |
/api/ext/v1/x/realtime/hub (SignalR, JSON): Join, Send, SendTo, Leave; olaylar member, message, state, closed | connect, send, sendTo, olaylar |
Örnek: iki kişilik XOX
Kurucu X, katılan O oynar. Tahta odanın paylaşılan durumundadır; hamleler update ile yazılır, herkes realtime.state olayından çizer. Emoji gibi geçici şeyler mesajla gider. render, showCode, showMenu, toast gibi arayüz işlevleri senindir.
{
"id": "kullanici-adin.xox",
"name": "XOX",
"version": "1.0.0",
"apiVersion": 1,
"minAppVersion": "0.3.1",
"description": { "tr": "Arkadaşınla iki kişilik XOX", "en": "Two-player tic-tac-toe with a friend" },
"storeCategory": "games",
"icon": "icon.svg",
"entry": { "ui": "index.html" },
"pages": [{ "id": "main", "title": { "tr": "XOX", "en": "Tic-tac-toe" } }],
"permissions": [
{ "nuvaka.realtime": true, "reason": { "tr": "Arkadaşınla aynı odada oynamak için", "en": "To play with your friend in the same room" } }
]
}
// app.js
const LINES = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
const result = (b) => LINES.map(([a, c, d]) => (b[a] && b[a] === b[c] && b[a] === b[d] ? b[a] : null)).find(Boolean)
?? (b.every(Boolean) ? 'draw' : null)
const fresh = () => ({ board: Array(9).fill(null), turn: 'X', result: null })
let roomId = null
let mine = null // 'X' | 'O'
function enter(room) {
roomId = room.roomId
mine = room.isCreator ? 'X' : 'O'
render(room.state) // null ise "rakip bekleniyor"
}
// Yeni oyun: oda kur, kodu göster, boş tahtayı yaz (yeni odada stateVersion 0)
async function host() {
const room = await nuvaka.realtime.create({ maxMembers: 2, meta: { game: 'xox' } })
enter(room)
showCode(room.code)
await nuvaka.realtime.setState(room.roomId, fresh(), { ifVersion: room.stateVersion })
}
async function joinByCode(code) {
try { enter(await nuvaka.realtime.join(code)) }
catch (e) { toast(e.code === 'room_full' ? 'Oda dolu' : 'Kod geçersiz ya da oda kapandı') }
}
// Hamle: sıra bendeyse ve kare boşsa yaz. Rakip araya girdiyse update tazesiyle yeniden dener.
async function play(i) {
await nuvaka.realtime.update(roomId, (s) => {
if (!s || s.result || s.turn !== mine || s.board[i]) return undefined
const board = s.board.slice()
board[i] = mine
return { board, turn: mine === 'X' ? 'O' : 'X', result: result(board) }
})
}
const rematch = () => nuvaka.realtime.update(roomId, (s) => (s?.result ? fresh() : undefined))
const sendEmoji = (v) => nuvaka.realtime.send(roomId, { t: 'emoji', v })
const inviteFriend = (username) => nuvaka.realtime.invite(roomId, username)
nuvaka.realtime.onState(({ roomId: id, state }) => { if (id === roomId) render(state) })
nuvaka.realtime.onMessage(({ roomId: id, from, data }) => {
if (id === roomId && data?.t === 'emoji') toast(`${from.displayName ?? from.username}: ${data.v}`)
})
nuvaka.realtime.onMember(({ roomId: id, type, username }) => {
if (id === roomId && type === 'left') toast(`${username} ayrıldı`)
})
nuvaka.realtime.onClosed(({ roomId: id }) => { if (id === roomId) { roomId = null; showMenu('Oda kapandı') } })
nuvaka.realtime.onInvite(async ({ roomId: id }) => enter(await nuvaka.realtime.connect(id)))
nuvaka.ready.then(async () => {
const invited = nuvaka.realtime.invitedRoom() // bildirimden açıldıysa doğrudan gir
if (!invited) return showMenu()
try { enter(await nuvaka.realtime.connect(invited)) }
catch (e) { showMenu(e.code === 'room_not_found' ? 'Oda kapanmış' : e.code) }
})
Kararları istemciler verdiği için bu yapı arkadaşlar arası oyunlara uygundur. Hile yapılamaması gereken oyunlarda kararı uzantı sunucusuna bırak.
Test: sahte nuvaka
@nuvaka/extension-sdk/testing odaları bellekte taklit eder: katılma kodu, üye sınırı, 5 oda, yalnız arkadaşa davet, MVCC, boyut sınırları ve gönderim hızı. Karşı tarafı m.peer ile oynatırsın.
| Araç | Açıklama |
|---|---|
realtime: { username, displayName, friends, friendsWithoutApp, invitedRoom } | seçenek: bu kullanıcı (varsayılan ben), davet edilebilen arkadaşlar, uygulaması olmayan arkadaşlar (not_installed), davet bağlantısıyla açılmış gibi |
m.peer.createRoom(username, opts?) | başkasının kurduğu oda: { roomId, code } |
m.peer.join / send / setState / offline / leave | başka bir üye katılır, mesaj gönderir, durumu yazar, bağlantısı kopar, ayrılır |
m.peer.invite(roomId, { open? }) | bu kullanıcıyı davet etmiş gibi; open: true açık sayfaya realtime.invite gönderir |
m.peer.close(roomId), m.peer.room(roomId) | oda kapanır / odanın hâli |
m.log.realtime, m.log.invites | gönderilen mesajlar (to: null = herkese) ve davetler |
import { it, expect, afterEach } from 'vitest'
import { installMockNuvaka, uninstallMockNuvaka } from '@nuvaka/extension-sdk/testing'
import { VersionConflictError } from '@nuvaka/extension-sdk'
afterEach(() => uninstallMockNuvaka())
it('rakip hamlesi, sürüm çakışması ve davet', async () => {
const m = installMockNuvaka({ grants: ['nuvaka.realtime'], realtime: { username: 'ayse', friends: ['veli'] } })
const room = await nuvaka.realtime.create({ maxMembers: 2 })
expect(room.code).toMatch(/^[A-Z0-9]{6}$/)
await nuvaka.realtime.setState(room.roomId, { board: Array(9).fill(null), turn: 'X', result: null }, { ifVersion: 0 })
const by = []
nuvaka.realtime.onState((e) => by.push(e.by))
await m.peer.join(room.roomId, 'veli')
const v = await m.peer.setState(room.roomId, 'veli', { board: ['O', null, null, null, null, null, null, null, null], turn: 'X', result: null })
expect(v).toBe(2)
expect(by).toEqual(['veli'])
// okuduğum sürüm (1) eskidi
await expect(nuvaka.realtime.setState(room.roomId, {}, { ifVersion: 1 })).rejects.toBeInstanceOf(VersionConflictError)
await expect(nuvaka.realtime.invite(room.roomId, 'ali')).rejects.toMatchObject({ code: 'not_friend' })
await nuvaka.realtime.send(room.roomId, { t: 'emoji', v: '👍' })
expect(m.log.realtime[0]).toMatchObject({ to: null, data: { t: 'emoji', v: '👍' } })
})
Gizlilik
- Odadakiler birbirinin yalnız kullanıcı adını ve görünen adını görür; e-posta ve kimlik paylaşılmaz.
- Mesaj içerikleri saklanmaz ve loglanmaz. Paylaşılan durum yalnız oda açıkken bellekte durur.
- Kullanıcıya odada ne paylaşıldığını açıkça göster; okuma izinleriyle birlikte istediğin
nuvaka.realtimeiçin gerekçeyi net yaz.
Nuvaka Apps API v1 · son güncelleme 2026-09-29