Frontend TS→JS ES modülleri + #42 codec altyapısı + versiyon握手
SDK (sdk/ dizini — native ES module, bundler yok): - sdk/version.js : SDK_VERSION="1.0.0", CODEC_JSON/BINARY sabitleri - sdk/codec.js : WSTSCodec — JSON (şu an), binary stub (#42 altyapısı) Binary çerçeveleme: opcode(1B)+length(4B)+payload; şimdilik NotImplemented fırlatır - sdk/EventTarget.js : MWSEEventTarget (browser EventTarget ile çakışma yok) - sdk/Connection.js : endpoint:"auto" → import.meta.url ile sunucu tespiti - sdk/WSTSProtocol.js : codec üzerinden encode/decode, sinyal yönlendirme - sdk/EventPool.js : request()/only() ayrımı (#33 korunuyor) - sdk/Peer.js : WebRTC stub bağlantısı, WOM pack/to (#33) - sdk/Room.js : WOM pack/room (#33), createRoom/join/eject - sdk/PeerInfo.js, RoomInfo.js, IPPressure.js, P2PFileSender.js - sdk/WebRTC.js : placeholder — ayrıca ele alınacak - sdk/index.js : MWSE ana sınıfı + versiyon el sıkışması Versiyon el sıkışması (backend ↔ frontend zorunlu): - internal/protocol/version.go : WSTSVersion="1.0.0", WSTSCodecJSON=0 - internal/services/yourid.go : bağlantı anında wsts/hello sinyali gönderir (v + codecs listesi; id sinyalinden ÖNCE gelir) - sdk/index.js _awaitHello() : scope çalıştırılmadan önce wsts/hello beklenir; versiyon uyuşmazsa bağlantı kapatılır + 'error' eventi ateşlenir (5s timeout) Servis (httpserver): - /sdk.js → 301 /sdk/index.js (import.meta.url doğru çözümlenir) - /sdk/ → sdk/ dizini file server - internal/config: MWSE_SDK_DIR env değişkeni (varsayılan ./sdk) Demo dosyaları: public/demos/{chat,audio,video}.html go test -race ./... yeşil Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
18269059cc
commit
28abefaaa9
162
README.md
162
README.md
|
|
@ -1,23 +1,161 @@
|
||||||
# MWSE Nedir?
|
# MWSE — Micro Web Socket Engine
|
||||||
|
|
||||||
MWSE yani Micro Web Socket Engine, kendisine bağlanan eşleri birbirleriyle ile eşleştirerek, eşler arası veri tünelleri oluşturan geniş ölçekli bir mikroservistir.
|
MWSE, kendisine bağlanan eşleri birbirleriyle eşleştirerek eşler arası veri
|
||||||
|
tünelleri oluşturan geniş ölçekli bir WebSocket mikroservis altyapısıdır.
|
||||||
|
|
||||||
Servis, bağlantı sağlayan cihazların verilerini kendi aralarında senkron etmek için kullanılabilir, cihazları gruplayabilir, odalar oluşturabilir, sohbet ve görüntülü görüşme yazılımları için alt yapı olarak kullanılabilir
|
Servis; cihazları senkronize etmek, odalar oluşturmak, sohbet ve görüntülü
|
||||||
|
görüşme yazılımları için gerçek zamanlı altyapı sağlamak amacıyla kullanılır.
|
||||||
|
Sunucu cihazları sanallaştırdığı için eşler birbirlerinin gerçek IP adresini veya
|
||||||
|
cihaz bilgisini bilmeden düşük gecikmeli, çift yönlü iletişim kurabilir.
|
||||||
|
|
||||||
Bağlantı TCP tabanlı yüksek hızlı WebSocket protokolüne dayanır ve sunucunun cihazları sanallaştırması sayesinde diğer kişilerin IP adreslerini veya cihaz türü gibi bilgilere ihtiyaç duymadan düşük gecikmeli çift taraflı serbest iletişim kurmalarını sağlar.
|
## Durum (Go engine, v0.1.0 paritesi ✅)
|
||||||
|
|
||||||
[Geliştirici Dökümanı](https://git.saqut.com/saqut/MWSE/wiki/Entegrasyon)
|
Motor Node.js'ten **Go** ile yeniden yazıldı. Concurrency modeli goroutine +
|
||||||
|
`sync.RWMutex` + bağlantı başına tek-yazıcı (actor) deseni üzerine kuruludur;
|
||||||
|
Node.js'teki "leave-while-send" race condition ve EventPool promise takılması
|
||||||
|
(#33) giderildi.
|
||||||
|
|
||||||
# Güvenlik !
|
| Özellik | Durum |
|
||||||
|
|---|---|
|
||||||
|
| WebSocket bağlantı yaşam döngüsü | ✅ |
|
||||||
|
| Oda oluşturma / katılma / çıkma | ✅ |
|
||||||
|
| Eşleme (pair) sistemi | ✅ |
|
||||||
|
| Paket tünelleme (pack/to, pack/room) | ✅ |
|
||||||
|
| Veri senkronizasyonu (data/sync, sync/pool) | ✅ |
|
||||||
|
| Bildirim + suit yanıtı (notify/send, notify/reply) | ✅ |
|
||||||
|
| 3. parti sunucu köprüsü (bridge) | ✅ |
|
||||||
|
| İkili çerçeveleme (binary framing) | ⏳ 2.5.0 |
|
||||||
|
| Studio (akış/WebRTC sunucusu) | ⏳ 2.0.0 |
|
||||||
|
|
||||||
Framework, bağlı tüm cihazlar arasında mesajları doğru hedefe, verinin bozulmadığını garanti ederek iletmekden sorumludur.
|
## Kurulum ve çalıştırma
|
||||||
|
|
||||||
Bunların dışında hassas verilerin soket üzerinden iletilmesi şimdilik önerilmez, clientlerin ileteceği mesajlar **SOKETE İLETİLMEDEN ÖNCE** kullanıcılar tarafından manipüle edilebilir veya taklit edilebilir ve MWSE bunun doğrulamasını **YAPMAZ**
|
### Gereksinimler
|
||||||
|
|
||||||
## WebSocket topolojisi
|
- Go 1.22+
|
||||||
|
- (Opsiyonel) TypeScript — SDK derlemek için `npm run build`
|
||||||
|
|
||||||

|
### Sunucuyu başlat
|
||||||
|
|
||||||
## Proje tarafından uygulanan load balance teknolojisi
|
```bash
|
||||||
|
# Bağımlılıklar
|
||||||
|
go mod tidy
|
||||||
|
|
||||||

|
# Çalıştır (varsayılan: 0.0.0.0:7707)
|
||||||
|
go run .
|
||||||
|
|
||||||
|
# Yapılandırma env değişkenleriyle:
|
||||||
|
MWSE_ADDR=:8080 \
|
||||||
|
MWSE_OUTBOUND_BUFFER=2048 \
|
||||||
|
MWSE_MAX_MESSAGE_SIZE=33554432 \
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ortam değişkenleri
|
||||||
|
|
||||||
|
| Değişken | Varsayılan | Açıklama |
|
||||||
|
|---|---|---|
|
||||||
|
| `MWSE_ADDR` | `:7707` | Dinleme adresi |
|
||||||
|
| `MWSE_OUTBOUND_BUFFER` | `1024` | Bağlantı başına gönderim kuyruğu |
|
||||||
|
| `MWSE_MAX_MESSAGE_SIZE` | `16777216` | Maksimum gelen frame boyutu (bayt) |
|
||||||
|
| `MWSE_PING_INTERVAL` | `10s` | Heartbeat ping aralığı |
|
||||||
|
| `BRIDGE_APPROVE_URL` | — | Bağlantı onay URL'i (3. parti köprü) |
|
||||||
|
| `BRIDGE_TRIGGER_URL` | — | Suit yanıtı push URL'i |
|
||||||
|
| `BRIDGE_INBOX` | — | `1` ile inbox'ı etkinleştir |
|
||||||
|
|
||||||
|
### Testler
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -race ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend SDK entegrasyonu
|
||||||
|
|
||||||
|
SDK, TypeScript ile yazılmış ve derlenmiş JavaScript olarak `/script`
|
||||||
|
endpoint'inden sunulur. Herhangi bir bundler gerekmez.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Sunucu çalışırken -->
|
||||||
|
<script src="http://localhost:7707/script"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const mwse = new MWSE({
|
||||||
|
endpoint: "ws://localhost:7707",
|
||||||
|
autoReconnect: true
|
||||||
|
});
|
||||||
|
|
||||||
|
mwse.on('scope', async () => {
|
||||||
|
console.log('Bağlandı:', mwse.me.socketId);
|
||||||
|
// Oda oluştur / katıl
|
||||||
|
const room = mwse.room({ name: 'genel', joinType: 'free', ifexistsJoin: true });
|
||||||
|
await room.createRoom();
|
||||||
|
room.on('message', (pack, peer) => console.log(peer.socketId, ':', pack));
|
||||||
|
room.send({ text: 'merhaba!' });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Demo dosyaları
|
||||||
|
|
||||||
|
| Demo | Yol | Açıklama |
|
||||||
|
|---|---|---|
|
||||||
|
| Chat | `/demos/chat.html` | ~20 satır JS ile odalı sohbet |
|
||||||
|
| Sesli görüşme | `/demos/audio.html` | P2P WebRTC ses (eşler arası) |
|
||||||
|
| Video görüşme | `/demos/video.html` | P2P WebRTC video (kamera ızgara görünümü) |
|
||||||
|
|
||||||
|
## API kontrolü (/api)
|
||||||
|
|
||||||
|
API anahtarı al, sonra kullan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Anahtar al
|
||||||
|
KEY=$(curl -s -X POST localhost:7707/api/auth/key \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"domain":"myapp"}' | jq -r .key)
|
||||||
|
|
||||||
|
# Tüm odaları listele
|
||||||
|
curl -s localhost:7707/api/rooms | jq .
|
||||||
|
|
||||||
|
# Belirli bir istemciye mesaj gönder
|
||||||
|
curl -s -X POST localhost:7707/api/client/<id>/send \
|
||||||
|
-H "x-api-key: $KEY" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"pack": {"hello": "world"}}'
|
||||||
|
|
||||||
|
# 3. parti köprü — inbox boşalt
|
||||||
|
curl -s -X POST localhost:7707/api/bridge/inbox \
|
||||||
|
-H "x-api-key: $KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mimari
|
||||||
|
|
||||||
|
```
|
||||||
|
Client (WebSocket)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ws.Hub (router + registry)
|
||||||
|
├─ services/auth.go Pairing, IP adresi, erişilebilirlik
|
||||||
|
├─ services/room.go Oda oluşturma / yönetimi
|
||||||
|
├─ services/datatransfer.go pack/to, request/to, response/to tünelleri
|
||||||
|
├─ services/notify.go Store-and-forward bildirim + suit yanıtı
|
||||||
|
├─ services/datastore.go Aktif senkronizasyon (CRUD broadcast)
|
||||||
|
│ Pasif senkronizasyon (hash-dedup merge pool)
|
||||||
|
├─ services/bridge.go 3. parti sunucu inbox (bridge/send)
|
||||||
|
└─ services/ippressure.go Sanal IP basıncı
|
||||||
|
|
||||||
|
httpserver
|
||||||
|
├─ GET/POST /api/* Kontrol düzlemi (API anahtarı, oda/istemci yönetimi)
|
||||||
|
├─ POST /api/bridge/inbox 3. parti sunucu inbox boşaltma
|
||||||
|
└─ /* (WebSocket değilse) SDK (script/index.js) + statik dosyalar
|
||||||
|
```
|
||||||
|
|
||||||
|
## Güvenlik
|
||||||
|
|
||||||
|
- Bağlı cihazların mesajları **sokete iletilmeden önce** kullanıcılar tarafından
|
||||||
|
manipüle edilebilir; MWSE bu doğrulamayı yapmaz. Hassas veriler için
|
||||||
|
uygulama katmanında imzalama/şifreleme ekleyin.
|
||||||
|
- 3. parti köprü (`BRIDGE_APPROVE_URL`) kullanılıyorsa bağlantı onayı uygulama
|
||||||
|
sunucusuna delege edilir (fail-closed: onay gelmezse bağlantı reddedilir).
|
||||||
|
|
||||||
|
## Geliştirici dökümanı
|
||||||
|
|
||||||
|
Tüm wiki sayfaları: <https://git.saqut.com/saqut/MWSE/wiki>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ type Config struct {
|
||||||
Port int // listen port, default 7707
|
Port int // listen port, default 7707
|
||||||
|
|
||||||
PublicDir string // static assets directory (default "./public")
|
PublicDir string // static assets directory (default "./public")
|
||||||
ScriptDir string // built SDK directory (default "./script")
|
ScriptDir string // legacy compiled SDK bundle directory (default "./script")
|
||||||
|
SDKDir string // ES-module SDK directory (default "./sdk")
|
||||||
|
|
||||||
ReadHeaderTimeout time.Duration // HTTP read-header timeout
|
ReadHeaderTimeout time.Duration // HTTP read-header timeout
|
||||||
ShutdownTimeout time.Duration // grace period for in-flight work on shutdown
|
ShutdownTimeout time.Duration // grace period for in-flight work on shutdown
|
||||||
|
|
@ -65,6 +66,7 @@ func Load() Config {
|
||||||
Port: envInt("MWSE_PORT", 7707),
|
Port: envInt("MWSE_PORT", 7707),
|
||||||
PublicDir: env("MWSE_PUBLIC_DIR", "./public"),
|
PublicDir: env("MWSE_PUBLIC_DIR", "./public"),
|
||||||
ScriptDir: env("MWSE_SCRIPT_DIR", "./script"),
|
ScriptDir: env("MWSE_SCRIPT_DIR", "./script"),
|
||||||
|
SDKDir: env("MWSE_SDK_DIR", "./sdk"),
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
ShutdownTimeout: time.Duration(envInt("MWSE_SHUTDOWN_TIMEOUT", 10)) * time.Second,
|
ShutdownTimeout: time.Duration(envInt("MWSE_SHUTDOWN_TIMEOUT", 10)) * time.Second,
|
||||||
TermOutput: envBool("MWSE_TERM_OUTPUT", false),
|
TermOutput: envBool("MWSE_TERM_OUTPUT", false),
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,9 @@ func New(hub *ws.Hub, cfg config.Config, srvOpts ...ServerOptions) *http.Server
|
||||||
|
|
||||||
// registerStatic wires the asset routes:
|
// registerStatic wires the asset routes:
|
||||||
//
|
//
|
||||||
// - /script -> the built SDK entry (script/index.js)
|
// - /sdk.js -> redirect to /sdk/index.js (so import.meta.url resolves correctly)
|
||||||
|
// - /sdk/ -> ES-module SDK files (sdk/EventTarget.js, etc.)
|
||||||
|
// - /script -> legacy compiled SDK entry (script/index.js)
|
||||||
// - /script/<file> -> files under the script directory
|
// - /script/<file> -> files under the script directory
|
||||||
// - / -> the SDK entry (so a bare visit returns the script)
|
// - / -> the SDK entry (so a bare visit returns the script)
|
||||||
// - /<file> -> a matching file under the public directory
|
// - /<file> -> a matching file under the public directory
|
||||||
|
|
@ -77,6 +79,13 @@ func registerStatic(mux *http.ServeMux, cfg config.Config) {
|
||||||
scriptIndex := filepath.Join(cfg.ScriptDir, "index.js")
|
scriptIndex := filepath.Join(cfg.ScriptDir, "index.js")
|
||||||
statusDoc := filepath.Join(cfg.ScriptDir, "status.xml")
|
statusDoc := filepath.Join(cfg.ScriptDir, "status.xml")
|
||||||
|
|
||||||
|
// ES-module SDK: redirect /sdk.js → /sdk/index.js so that import.meta.url
|
||||||
|
// resolves to /sdk/index.js and all relative imports go to /sdk/*.
|
||||||
|
mux.HandleFunc("/sdk.js", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/sdk/index.js", http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
|
mux.Handle("/sdk/", http.StripPrefix("/sdk/", http.FileServer(http.Dir(cfg.SDKDir))))
|
||||||
|
|
||||||
mux.HandleFunc("/script", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/script", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeFile(w, r, scriptIndex)
|
http.ServeFile(w, r, scriptIndex)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package protocol
|
||||||
|
|
||||||
|
// WSTSVersion is the wire protocol version. The SDK checks this against its own
|
||||||
|
// version constant (sdk/version.js) on every connection: if they differ the SDK
|
||||||
|
// refuses to proceed. Bump both together when making a breaking wire change.
|
||||||
|
//
|
||||||
|
// Current versioning scheme:
|
||||||
|
// "1.x.x" JSON text frames (codec id 0) — v0.1.0 → current
|
||||||
|
// "2.x.x" Binary frames (codec id 1) — planned, issue #42
|
||||||
|
const WSTSVersion = "1.0.0"
|
||||||
|
|
||||||
|
// WSTSCodecJSON is the codec identifier for the current JSON text framing.
|
||||||
|
// The server lists supported codecs in the wsts/hello signal so the client can
|
||||||
|
// negotiate the best mode it understands.
|
||||||
|
const WSTSCodecJSON = 0
|
||||||
|
|
@ -1,11 +1,25 @@
|
||||||
package services
|
package services
|
||||||
|
|
||||||
import "git.saqut.com/saqut/mwse/internal/ws"
|
import (
|
||||||
|
"git.saqut.com/saqut/mwse/internal/protocol"
|
||||||
|
"git.saqut.com/saqut/mwse/internal/ws"
|
||||||
|
)
|
||||||
|
|
||||||
// registerYourID tells a freshly connected client its own socket id, exactly as
|
// registerYourID sends two signals on every new connection:
|
||||||
// the Node YourID service did: client.send([{type:'id', value: id}, 'id']).
|
//
|
||||||
|
// 1. wsts/hello — version handshake. The SDK checks this version against its own
|
||||||
|
// constant (sdk/version.js) and refuses to proceed if they differ. The codecs
|
||||||
|
// list advertises which frame encodings the server supports (0 = JSON, 1 =
|
||||||
|
// binary once #42 is ready). This signal is sent first so the SDK can gate
|
||||||
|
// all subsequent processing on a successful version check.
|
||||||
|
//
|
||||||
|
// 2. id — the client's own socket id, exactly as the original Node YourID did.
|
||||||
func registerYourID(hub *ws.Hub) {
|
func registerYourID(hub *ws.Hub) {
|
||||||
hub.OnConnect(func(c *ws.Client) {
|
hub.OnConnect(func(c *ws.Client) {
|
||||||
|
c.Signal("wsts/hello", map[string]any{
|
||||||
|
"v": protocol.WSTSVersion,
|
||||||
|
"codecs": []int{protocol.WSTSCodecJSON},
|
||||||
|
})
|
||||||
c.Signal("id", map[string]any{"type": "id", "value": c.ID})
|
c.Signal("id", map[string]any{"type": "id", "value": c.ID})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="tr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>MWSE — Sesli Görüşme Demo</title>
|
||||||
|
<script src="/script"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 700px; margin: 2rem auto; padding: 0 1rem; }
|
||||||
|
h2 { margin-bottom: .3rem; }
|
||||||
|
.note { font-size: .85rem; color: #666; margin-bottom: 1rem; }
|
||||||
|
#status { padding: 6px 10px; background: #f5f5f5; border-radius: 4px;
|
||||||
|
font-size: .9rem; margin-bottom: 1rem; }
|
||||||
|
#peers { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||||
|
.peer-card {
|
||||||
|
border: 1px solid #bbb; border-radius: 6px; padding: 10px 14px;
|
||||||
|
min-width: 160px; background: #fff;
|
||||||
|
}
|
||||||
|
.peer-card .id { font-family: monospace; font-size: .8rem; color: #555; }
|
||||||
|
.peer-card .st { font-size: .85rem; margin-top: 4px; }
|
||||||
|
.peer-card button { margin-top: 8px; padding: 4px 10px; cursor: pointer; }
|
||||||
|
audio { display: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>MWSE Sesli Görüşme Demo</h2>
|
||||||
|
<p class="note">
|
||||||
|
Aynı anda birden fazla sekme ya da kullanıcı açın. Odaya katılan herkese
|
||||||
|
otomatik çift yönlü ses bağlantısı kurulur (P2P WebRTC, max ~10–15 kişi için
|
||||||
|
mesh topolojisi; daha fazlası için SRS entegrasyonu gerekir).
|
||||||
|
</p>
|
||||||
|
<div id="status">Bağlanıyor…</div>
|
||||||
|
<div id="peers"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Akış: MWSE bağlan → odaya katıl → gelen eşle eşleş →
|
||||||
|
// mikrofon aç → WebRTC ses kanalını başlat.
|
||||||
|
|
||||||
|
const mwse = new MWSE({ endpoint: location.origin.replace(/^http/, 'ws') });
|
||||||
|
let localStream; // MediaStream: mikrofon
|
||||||
|
const cards = {}; // socketId → DOM element
|
||||||
|
|
||||||
|
// ---- Bağlantı -------------------------------------------------------
|
||||||
|
|
||||||
|
mwse.on('scope', async () => {
|
||||||
|
status(`Bağlandı: ${mwse.me.socketId}`);
|
||||||
|
|
||||||
|
// Mikrofon erişimi: kullanıcı iznine gerek var.
|
||||||
|
try {
|
||||||
|
localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
} catch (err) {
|
||||||
|
status(`Mikrofon erişimi reddedildi: ${err}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "sesli" odasına katıl (varsa mevcut odaya gir, yoksa oluştur).
|
||||||
|
const room = mwse.room({ name: 'sesli', joinType: 'free', ifexistsJoin: true });
|
||||||
|
await room.createRoom();
|
||||||
|
status(`Odada: sesli | Kimliğim: ${mwse.me.socketId}`);
|
||||||
|
|
||||||
|
// Odaya yeni biri katıldığında — biz eşleme isteği atalım.
|
||||||
|
room.on('join', async peer => {
|
||||||
|
upsertCard(peer.socketId, 'eşleniyor…');
|
||||||
|
await peer.requestPair();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Birisi bize eşleme isteği gönderdiğinde — otomatik kabul.
|
||||||
|
mwse.me.on('request/pair', async peer => {
|
||||||
|
await peer.acceptPair();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eşleme kabul edildiğinde — WebRTC sesini başlat.
|
||||||
|
mwse.me.on('accepted/pair', peer => {
|
||||||
|
startAudio(peer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eş bağlantıyı kestiğinde kart güncelle.
|
||||||
|
mwse.me.on('end/pair', (peerId) => {
|
||||||
|
upsertCard(peerId, 'ayrıldı');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- WebRTC ses kurulumu -------------------------------------------
|
||||||
|
|
||||||
|
function startAudio(peer) {
|
||||||
|
upsertCard(peer.socketId, 'bağlanıyor…');
|
||||||
|
|
||||||
|
// Peer.rtc WebRTC.ts'deki mevcut örnek; signaling MWSE üzerinden gider.
|
||||||
|
const rtc = peer.rtc;
|
||||||
|
rtc.connect(); // varsayılan data kanalını aç (sinyal trafiği için)
|
||||||
|
|
||||||
|
// Ses akışını peer'a gönder.
|
||||||
|
rtc.sendStream(localStream, 'audio', { kind: 'audio' });
|
||||||
|
|
||||||
|
// Uzak ses akışı gelince çal.
|
||||||
|
rtc.on('stream:added', ({ stream, name }) => {
|
||||||
|
if (name !== 'audio') return;
|
||||||
|
const audio = document.createElement('audio');
|
||||||
|
audio.autoplay = true;
|
||||||
|
audio.srcObject = stream;
|
||||||
|
document.body.appendChild(audio);
|
||||||
|
upsertCard(peer.socketId, `🔊 konuşuyor`);
|
||||||
|
});
|
||||||
|
|
||||||
|
rtc.on('connected', () => upsertCard(peer.socketId, '🟢 bağlandı'));
|
||||||
|
rtc.on('disconnected', () => upsertCard(peer.socketId, '🔴 kesildi'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- UI yardımcıları -----------------------------------------------
|
||||||
|
|
||||||
|
function upsertCard(id, state) {
|
||||||
|
if (!cards[id]) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'peer-card';
|
||||||
|
card.innerHTML = `<div class="id">${id}</div><div class="st"></div>`;
|
||||||
|
document.getElementById('peers').appendChild(card);
|
||||||
|
cards[id] = card;
|
||||||
|
}
|
||||||
|
cards[id].querySelector('.st').textContent = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(msg) {
|
||||||
|
document.getElementById('status').textContent = msg;
|
||||||
|
console.log('[audio-demo]', msg);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="tr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>MWSE — Chat Demo</title>
|
||||||
|
<script src="/script"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
|
||||||
|
h2 { margin-bottom: .5rem; }
|
||||||
|
#status { font-size: .85rem; color: #666; margin-bottom: .5rem; }
|
||||||
|
#log { border: 1px solid #ccc; height: 260px; overflow-y: auto; padding: 8px;
|
||||||
|
margin-bottom: 8px; border-radius: 4px; }
|
||||||
|
#log p { margin: 2px 0; font-size: .9rem; }
|
||||||
|
#log .system { color: #888; font-style: italic; }
|
||||||
|
#log .me { color: #006; font-weight: bold; }
|
||||||
|
#log .peer { color: #060; }
|
||||||
|
#controls { display: flex; gap: 6px; }
|
||||||
|
#msg { flex: 1; padding: 6px; }
|
||||||
|
button { padding: 6px 14px; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>MWSE Chat Demo</h2>
|
||||||
|
<p id="status">Bağlanıyor…</p>
|
||||||
|
<div id="log"></div>
|
||||||
|
<div id="controls">
|
||||||
|
<input id="msg" placeholder="Mesaj yaz… (Enter)" onkeydown="if(event.key==='Enter') send()">
|
||||||
|
<button onclick="send()">Gönder</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Bu demo ~20 satır JavaScript ile odalı gerçek zamanlı sohbet kurar.
|
||||||
|
// Birden fazla sekme / kullanıcı aynı "genel" odasına otomatik katılır.
|
||||||
|
|
||||||
|
const mwse = new MWSE({ endpoint: location.origin.replace(/^http/, 'ws') });
|
||||||
|
let room;
|
||||||
|
|
||||||
|
mwse.on('scope', async () => {
|
||||||
|
document.getElementById('status').textContent =
|
||||||
|
`Bağlandı: ${mwse.me.socketId}`;
|
||||||
|
|
||||||
|
room = mwse.room({ name: 'genel', joinType: 'free', ifexistsJoin: true });
|
||||||
|
await room.createRoom();
|
||||||
|
|
||||||
|
room.on('join', peer => log(`${peer.socketId} odaya katıldı`, 'system'));
|
||||||
|
room.on('eject', peer => log(`${peer.socketId} odadan ayrıldı`, 'system'));
|
||||||
|
room.on('message', (pack, peer) => log(`${peer.socketId}: ${pack.text}`, 'peer'));
|
||||||
|
});
|
||||||
|
|
||||||
|
function send() {
|
||||||
|
const el = document.getElementById('msg');
|
||||||
|
const text = el.value.trim();
|
||||||
|
if (!text || !room) return;
|
||||||
|
room.send({ text });
|
||||||
|
log(`ben: ${text}`, 'me');
|
||||||
|
el.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(text, cls = '') {
|
||||||
|
const p = Object.assign(document.createElement('p'), { textContent: text, className: cls });
|
||||||
|
const box = document.getElementById('log');
|
||||||
|
box.appendChild(p);
|
||||||
|
box.scrollTop = box.scrollHeight;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="tr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>MWSE — Video Demo</title>
|
||||||
|
<script src="/script"></script>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: sans-serif; margin: 0; background: #111; color: #eee; }
|
||||||
|
#header { padding: 10px 16px; background: #1a1a1a; display: flex; align-items: center; gap: 12px; }
|
||||||
|
#header h2 { margin: 0; font-size: 1.1rem; }
|
||||||
|
#status { font-size: .8rem; color: #aaa; }
|
||||||
|
#grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 6px; padding: 10px;
|
||||||
|
}
|
||||||
|
.tile {
|
||||||
|
background: #222; border-radius: 6px; overflow: hidden; position: relative;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
}
|
||||||
|
.tile video { width: 100%; height: 100%; object-fit: cover; background: #000; }
|
||||||
|
.tile .label {
|
||||||
|
position: absolute; bottom: 6px; left: 6px;
|
||||||
|
font-size: .75rem; background: rgba(0,0,0,.6); padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.tile .indicator {
|
||||||
|
position: absolute; top: 6px; right: 6px; width: 10px; height: 10px;
|
||||||
|
border-radius: 50%; background: #f44;
|
||||||
|
}
|
||||||
|
.tile.connected .indicator { background: #4c4; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="header">
|
||||||
|
<h2>MWSE Video Demo</h2>
|
||||||
|
<span id="status">Bağlanıyor…</span>
|
||||||
|
</div>
|
||||||
|
<div id="grid" id="grid"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Akış: MWSE bağlan → kamera/mikrofon aç → kendi videoyu göster →
|
||||||
|
// odaya katıl → her yeni eşle P2P WebRTC video kurulumu yap.
|
||||||
|
//
|
||||||
|
// Ölçek notu: WebRTC mesh topolojisi ~6–8 kişiye kadar makul çalışır.
|
||||||
|
// Daha büyük odalar (100–500 kişi) için SRS tabanlı SFU gereklidir (#39).
|
||||||
|
// Bu demo o SFU mimarisinin istemci tarafı API kalıplarını gösterir.
|
||||||
|
|
||||||
|
const mwse = new MWSE({ endpoint: location.origin.replace(/^http/, 'ws') });
|
||||||
|
let localStream;
|
||||||
|
const tiles = {}; // socketId → { el, rtc }
|
||||||
|
|
||||||
|
// ---- Kamera/mikrofon ------------------------------------------------
|
||||||
|
|
||||||
|
mwse.on('scope', async () => {
|
||||||
|
status(`Bağlandı: ${mwse.me.socketId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
|
||||||
|
} catch (err) {
|
||||||
|
status(`Kamera erişimi reddedildi: ${err}`);
|
||||||
|
addTile('ben (kamera yok)', null, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kendi videoyu göster (sessiz — aksi hâlde geri besleme oluşur).
|
||||||
|
addTile(`ben (${mwse.me.socketId.slice(-6)})`, localStream, true);
|
||||||
|
|
||||||
|
// Odaya katıl.
|
||||||
|
const room = mwse.room({ name: 'video', joinType: 'free', ifexistsJoin: true });
|
||||||
|
await room.createRoom();
|
||||||
|
status(`Odada: video | ${mwse.me.socketId}`);
|
||||||
|
|
||||||
|
// Yeni katılan ile bağlantı başlat.
|
||||||
|
room.on('join', async peer => {
|
||||||
|
setTileState(peer.socketId, false);
|
||||||
|
await peer.requestPair();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Otomatik eşleşme kabul.
|
||||||
|
mwse.me.on('request/pair', async peer => {
|
||||||
|
setTileState(peer.socketId, false);
|
||||||
|
await peer.acceptPair();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eşleme tamam — WebRTC video başlat.
|
||||||
|
mwse.me.on('accepted/pair', peer => startVideo(peer));
|
||||||
|
|
||||||
|
// Eş ayrıldı.
|
||||||
|
mwse.me.on('end/pair', id => removeTile(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- WebRTC video kurulumu -----------------------------------------
|
||||||
|
|
||||||
|
function startVideo(peer) {
|
||||||
|
const rtc = peer.rtc;
|
||||||
|
rtc.connect();
|
||||||
|
|
||||||
|
// Kendi videomuzun tüm izlerini peer'a gönder.
|
||||||
|
if (localStream) rtc.sendStream(localStream, 'video', { kind: 'video' });
|
||||||
|
|
||||||
|
// Uzak video gelince tile'a yerleştir.
|
||||||
|
rtc.on('stream:added', ({ stream, name }) => {
|
||||||
|
if (name !== 'video') return;
|
||||||
|
const label = `${peer.socketId.slice(-6)}`;
|
||||||
|
addTile(label, stream, false);
|
||||||
|
setTileState(peer.socketId, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
rtc.on('connected', () => setTileState(peer.socketId, true));
|
||||||
|
rtc.on('disconnected', () => setTileState(peer.socketId, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- UI -----------------------------------------------------------
|
||||||
|
|
||||||
|
function addTile(label, stream, isMe) {
|
||||||
|
const id = isMe ? 'me' : label;
|
||||||
|
if (tiles[id]) return;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tile' + (isMe ? ' connected' : '');
|
||||||
|
div.innerHTML = `
|
||||||
|
<video autoplay playsinline ${isMe ? 'muted' : ''}></video>
|
||||||
|
<div class="label">${label}</div>
|
||||||
|
<div class="indicator"></div>`;
|
||||||
|
if (stream) div.querySelector('video').srcObject = stream;
|
||||||
|
document.getElementById('grid').appendChild(div);
|
||||||
|
tiles[id] = div;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTileState(peerId, connected) {
|
||||||
|
const tile = tiles[peerId.slice(-6)] || tiles[peerId];
|
||||||
|
if (tile) tile.classList.toggle('connected', connected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTile(peerId) {
|
||||||
|
const key = (peerId || '').slice(-6);
|
||||||
|
const tile = tiles[key] || tiles[peerId];
|
||||||
|
if (tile) { tile.remove(); delete tiles[key]; delete tiles[peerId]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(msg) {
|
||||||
|
document.getElementById('status').textContent = msg;
|
||||||
|
console.log('[video-demo]', msg);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
// WebSocket lifecycle management.
|
||||||
|
// import.meta.url is used for the 'auto' endpoint mode so the SDK always
|
||||||
|
// connects back to the same server it was downloaded from.
|
||||||
|
|
||||||
|
export class Connection {
|
||||||
|
constructor(mwse, options) {
|
||||||
|
this.mwse = mwse;
|
||||||
|
this.connected = false;
|
||||||
|
this.autoPair = false;
|
||||||
|
this.autoReconnect = true;
|
||||||
|
this.autoReconnectTimeout = 3000;
|
||||||
|
this.autoReconnectTimer = undefined;
|
||||||
|
|
||||||
|
this._activeCallbacks = [];
|
||||||
|
this._passiveCallbacks = [];
|
||||||
|
this._packCallbacks = [];
|
||||||
|
|
||||||
|
if (options.endpoint === 'auto') {
|
||||||
|
// In ES modules document.currentScript is null; use import.meta.url
|
||||||
|
// instead — it resolves to the URL the SDK was actually loaded from.
|
||||||
|
const scriptURL = new URL(import.meta.url);
|
||||||
|
const isSecure = scriptURL.protocol === 'https:';
|
||||||
|
scriptURL.protocol = isSecure ? 'wss:' : 'ws:';
|
||||||
|
// Strip /index.js (or any filename) so we connect to the server root.
|
||||||
|
scriptURL.pathname = scriptURL.pathname.replace(/\/[^/]+$/, '/');
|
||||||
|
this.endpoint = scriptURL;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
this.endpoint = new URL(options.endpoint);
|
||||||
|
} catch {
|
||||||
|
throw new Error('MWSE: endpoint is required and must be a valid URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.autoReconnect === 'boolean') {
|
||||||
|
this.autoReconnect = options.autoReconnect;
|
||||||
|
} else if (options.autoReconnect) {
|
||||||
|
this.autoReconnect = true;
|
||||||
|
this.autoReconnectTimeout = options.autoReconnect.timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (this.autoReconnectTimer) clearTimeout(this.autoReconnectTimer);
|
||||||
|
this.ws = new WebSocket(this.endpoint.href);
|
||||||
|
this._attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
// Prevent auto-reconnect when the caller explicitly closes.
|
||||||
|
this.autoReconnect = false;
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_attachEvents() {
|
||||||
|
this.ws.addEventListener('open', () => this._onOpen());
|
||||||
|
this.ws.addEventListener('close', () => this._onClose());
|
||||||
|
this.ws.addEventListener('error', () => this._onError());
|
||||||
|
this.ws.addEventListener('message', ({ data }) => this._onMessage(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
_onOpen() {
|
||||||
|
this.connected = true;
|
||||||
|
for (const cb of this._activeCallbacks) cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClose() {
|
||||||
|
for (const cb of this._passiveCallbacks) cb();
|
||||||
|
this.connected = false;
|
||||||
|
if (this.autoReconnect) {
|
||||||
|
this.autoReconnectTimer = setTimeout(
|
||||||
|
() => this.connect(),
|
||||||
|
this.autoReconnectTimeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onError() {
|
||||||
|
this.connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMessage(data) {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
for (const cb of this._packCallbacks) cb(parsed);
|
||||||
|
} else if (data instanceof ArrayBuffer) {
|
||||||
|
// Binary frame — passed raw to WSTSProtocol for codec decoding.
|
||||||
|
for (const cb of this._packCallbacks) cb(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRecaivePack(fn) { this._packCallbacks.push(fn); }
|
||||||
|
|
||||||
|
onActive(fn) {
|
||||||
|
if (this.connected) fn();
|
||||||
|
else this._activeCallbacks.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPassive(fn) {
|
||||||
|
if (!this.connected) fn();
|
||||||
|
else this._passiveCallbacks.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
tranferToServer(data) {
|
||||||
|
if (this.connected) this.ws.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// EventPool: pending-request registry and signal fan-out.
|
||||||
|
//
|
||||||
|
// Two distinct paths (see issue #33 for why the split matters):
|
||||||
|
// request() — sends a packet AND registers a waiter for the correlated reply.
|
||||||
|
// Use ONLY for handlers that return a reply (my/socketid, joinroom, …).
|
||||||
|
// only() — sends a packet with NO registered waiter (fire-and-forget / WOM).
|
||||||
|
// Use for pack/to, pack/room without handshake, etc. — anything the
|
||||||
|
// engine answers nil to, so there is no reply to wait for.
|
||||||
|
// Confusing the two leaves orphaned Promises or swallowed replies (#33).
|
||||||
|
|
||||||
|
export default class EventPool {
|
||||||
|
constructor(mwse) {
|
||||||
|
this.wsts = mwse;
|
||||||
|
this.events = new Map(); // id → [resolve, reject]
|
||||||
|
this.signals = new Map(); // signalName → [callback, …]
|
||||||
|
this.count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// request: sends msg and returns a Promise resolved by the server's reply.
|
||||||
|
request(msg) {
|
||||||
|
return new Promise((ok, rej) => {
|
||||||
|
const id = ++this.count;
|
||||||
|
this.events.set(id, [ok, rej]);
|
||||||
|
this.wsts.WSTSProtocol.SendRequest(msg, id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// only: sends msg without registering a waiter (WOM / fire-and-forget).
|
||||||
|
only(msg) {
|
||||||
|
this.wsts.WSTSProtocol.SendOnly(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// stream: sends msg and calls callback for every 'C'ontinue reply.
|
||||||
|
stream(msg, callback) {
|
||||||
|
const id = ++this.count;
|
||||||
|
this.wsts.WSTSProtocol.StartStream(msg, id);
|
||||||
|
this.events.set(id, [callback, () => {}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// signal: registers a listener for a named server-initiated signal.
|
||||||
|
signal(event, callback) {
|
||||||
|
const existing = this.signals.get(event);
|
||||||
|
if (existing) {
|
||||||
|
existing.push(callback);
|
||||||
|
} else {
|
||||||
|
this.signals.set(event, [callback]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Minimal event emitter used as a base class throughout the SDK.
|
||||||
|
// Named MWSEEventTarget to avoid collision with the browser's built-in EventTarget.
|
||||||
|
export default class MWSEEventTarget {
|
||||||
|
constructor() {
|
||||||
|
this._events = {};
|
||||||
|
this.activeScope = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(eventName, ...args) {
|
||||||
|
const listeners = this._events[eventName];
|
||||||
|
if (listeners) {
|
||||||
|
for (const cb of listeners) cb(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on(eventName, callback) {
|
||||||
|
if (this._events[eventName]) {
|
||||||
|
this._events[eventName].push(callback);
|
||||||
|
} else {
|
||||||
|
this._events[eventName] = [callback];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scope(f) fires f immediately when already in scope, otherwise queues it for
|
||||||
|
// the next 'scope' event — same convenience the original TS SDK provided.
|
||||||
|
scope(f) {
|
||||||
|
if (this.activeScope) {
|
||||||
|
f();
|
||||||
|
} else {
|
||||||
|
this.on('scope', f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
// Virtual address / IP pressure system.
|
||||||
|
// Lets a connection claim a human-friendly alias (short code, number, IP-style address).
|
||||||
|
|
||||||
|
export class IPPressure {
|
||||||
|
constructor(mwse) {
|
||||||
|
this.mwse = mwse;
|
||||||
|
this.APNumber = undefined;
|
||||||
|
this.APShortCode = undefined;
|
||||||
|
this.APIPAddress = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async allocAPIPAddress() {
|
||||||
|
const { status, ip } = await this.mwse.EventPooling.request({ type: 'alloc/APIPAddress' });
|
||||||
|
if (status === 'success') { this.APIPAddress = ip; return ip; }
|
||||||
|
throw new Error('Error allocating virtual IP address');
|
||||||
|
}
|
||||||
|
|
||||||
|
async allocAPNumber() {
|
||||||
|
const { status, number } = await this.mwse.EventPooling.request({ type: 'alloc/APNumber' });
|
||||||
|
if (status === 'success') { this.APNumber = number; return number; }
|
||||||
|
throw new Error('Error allocating virtual number');
|
||||||
|
}
|
||||||
|
|
||||||
|
async allocAPShortCode() {
|
||||||
|
const { status, code } = await this.mwse.EventPooling.request({ type: 'alloc/APShortCode' });
|
||||||
|
if (status === 'success') { this.APShortCode = code; return code; }
|
||||||
|
throw new Error('Error allocating virtual short code');
|
||||||
|
}
|
||||||
|
|
||||||
|
async reallocAPIPAddress() {
|
||||||
|
const { status, ip } = await this.mwse.EventPooling.request({ type: 'realloc/APIPAddress' });
|
||||||
|
if (status === 'success') { this.APIPAddress = ip; return ip; }
|
||||||
|
throw new Error('Error reallocating virtual IP address');
|
||||||
|
}
|
||||||
|
|
||||||
|
async reallocAPNumber() {
|
||||||
|
const { status, number } = await this.mwse.EventPooling.request({ type: 'realloc/APNumber' });
|
||||||
|
if (status === 'success') { this.APNumber = number; return number; }
|
||||||
|
throw new Error('Error reallocating virtual number');
|
||||||
|
}
|
||||||
|
|
||||||
|
async reallocAPShortCode() {
|
||||||
|
const { status, code } = await this.mwse.EventPooling.request({ type: 'realloc/APShortCode' });
|
||||||
|
if (status === 'success') { this.APShortCode = code; return code; }
|
||||||
|
throw new Error('Error reallocating virtual short code');
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseAPIPAddress() {
|
||||||
|
const { status } = await this.mwse.EventPooling.request({ type: 'release/APIPAddress' });
|
||||||
|
if (status === 'success') { this.APIPAddress = undefined; return; }
|
||||||
|
throw new Error('Error releasing virtual IP address');
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseAPNumber() {
|
||||||
|
const { status } = await this.mwse.EventPooling.request({ type: 'release/APNumber' });
|
||||||
|
if (status === 'success') { this.APNumber = undefined; return; }
|
||||||
|
throw new Error('Error releasing virtual number');
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseAPShortCode() {
|
||||||
|
const { status } = await this.mwse.EventPooling.request({ type: 'release/APShortCode' });
|
||||||
|
if (status === 'success') { this.APShortCode = undefined; return; }
|
||||||
|
throw new Error('Error releasing virtual short code');
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryAPIPAddress(ip) {
|
||||||
|
const { status, socket } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'whois/APIPAddress', whois: ip
|
||||||
|
});
|
||||||
|
return status === 'success' ? socket : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryAPNumber(number) {
|
||||||
|
const { status, socket } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'whois/APNumber', whois: number
|
||||||
|
});
|
||||||
|
return status === 'success' ? socket : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryAPShortCode(code) {
|
||||||
|
const { status, socket } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'whois/APShortCode', whois: code
|
||||||
|
});
|
||||||
|
return status === 'success' ? socket : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
// P2P file transfer over WebRTC data channels. Experimental — do not use in
|
||||||
|
// production without testing. Requires a live WebRTC peer connection.
|
||||||
|
|
||||||
|
export default class P2PFileSender {
|
||||||
|
constructor(webrtc, peer) {
|
||||||
|
this.webrtc = webrtc;
|
||||||
|
this.rtc = webrtc.rtc;
|
||||||
|
this.peer = peer;
|
||||||
|
|
||||||
|
this.totalSize = 0;
|
||||||
|
this.isReady = false;
|
||||||
|
this.isStarted = false;
|
||||||
|
this.isSending = false;
|
||||||
|
this.isRecaiving = false;
|
||||||
|
this.processedSize = 0;
|
||||||
|
|
||||||
|
this.bufferSizePerChannel = 10e6;
|
||||||
|
this.bufferSizePerPack = 10e3;
|
||||||
|
this.safeBufferSizePerPack = 10e3 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async RecaiveFile(fileMetadata, channelCount, _totalSize, onEnded) {
|
||||||
|
let parts = [];
|
||||||
|
this.webrtc.on('datachannel', datachannel => {
|
||||||
|
let current = 0;
|
||||||
|
let totalSize = 0;
|
||||||
|
let currentPart = 0;
|
||||||
|
let bufferAmount = [];
|
||||||
|
|
||||||
|
datachannel.onmessage = ({ data }) => {
|
||||||
|
if (totalSize === 0) {
|
||||||
|
const { size, part } = JSON.parse(data);
|
||||||
|
totalSize = size;
|
||||||
|
currentPart = part;
|
||||||
|
datachannel.send('READY');
|
||||||
|
} else {
|
||||||
|
current += data.byteLength;
|
||||||
|
bufferAmount.push(data);
|
||||||
|
if (current === totalSize) {
|
||||||
|
parts[currentPart] = new Blob(bufferAmount);
|
||||||
|
bufferAmount = [];
|
||||||
|
totalSize = 0;
|
||||||
|
currentPart = 0;
|
||||||
|
current = 0;
|
||||||
|
datachannel.send('TOTAL_RECAIVED');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
datachannel.onclose = () => {
|
||||||
|
channelCount--;
|
||||||
|
if (channelCount === 0) {
|
||||||
|
const file = new File(parts, fileMetadata.name, {
|
||||||
|
type: fileMetadata.type,
|
||||||
|
lastModified: Date.now()
|
||||||
|
});
|
||||||
|
onEnded(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async SendFile(file, metadata) {
|
||||||
|
this.isSending = true;
|
||||||
|
this.isStarted = true;
|
||||||
|
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const partCount = Math.ceil(buffer.byteLength / 10e6);
|
||||||
|
const channelCount = Math.min(5, partCount);
|
||||||
|
|
||||||
|
if (this.webrtc.iceStatus !== 'connected') {
|
||||||
|
throw new Error('WebRTC is not ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.peer.send({
|
||||||
|
type: 'file',
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
mimetype: file.type,
|
||||||
|
partCount,
|
||||||
|
channelCount,
|
||||||
|
metadata
|
||||||
|
});
|
||||||
|
|
||||||
|
const channels = [];
|
||||||
|
for (let i = 0; i < channelCount; i++) {
|
||||||
|
const channel = this.rtc.createDataChannel('\\?\\file_' + i);
|
||||||
|
channel.binaryType = 'arraybuffer';
|
||||||
|
await new Promise(ok => { channel.onopen = () => ok(); });
|
||||||
|
channels.push(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentPart = 0;
|
||||||
|
const next = () => {
|
||||||
|
if (currentPart < partCount) {
|
||||||
|
const part = buffer.slice(currentPart * 10e6, (currentPart + 1) * 10e6);
|
||||||
|
return [part, currentPart++];
|
||||||
|
}
|
||||||
|
return [false, 0];
|
||||||
|
};
|
||||||
|
|
||||||
|
let pending = channels.length;
|
||||||
|
await new Promise(ok => {
|
||||||
|
for (let i = 0; i < channels.length; i++) {
|
||||||
|
this._sendPartition(channels[i], next, () => {
|
||||||
|
if (--pending === 0) {
|
||||||
|
this.isSending = false;
|
||||||
|
this.isStarted = false;
|
||||||
|
ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendPartition(channel, nextblob, onEnded) {
|
||||||
|
let [currentBuffer, currentPartition] = nextblob();
|
||||||
|
let currentPart = 0;
|
||||||
|
|
||||||
|
const nextChunk = () => {
|
||||||
|
if (!(currentBuffer instanceof ArrayBuffer)) return;
|
||||||
|
const chunk = currentBuffer.slice(currentPart * 16e3, (currentPart + 1) * 16e3);
|
||||||
|
currentPart++;
|
||||||
|
return chunk.byteLength ? chunk : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.addEventListener('message', ({ data }) => {
|
||||||
|
if (data === 'READY') {
|
||||||
|
this._sendChannel(channel, nextChunk);
|
||||||
|
} else if (data === 'TOTAL_RECAIVED') {
|
||||||
|
[currentBuffer, currentPartition] = nextblob();
|
||||||
|
currentPart = 0;
|
||||||
|
if (currentBuffer !== false) {
|
||||||
|
channel.send(JSON.stringify({ size: currentBuffer.byteLength, part: currentPartition }));
|
||||||
|
} else {
|
||||||
|
channel.close();
|
||||||
|
onEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
channel.send(JSON.stringify({ size: currentBuffer.byteLength, part: currentPartition }));
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendChannel(channel, getNextChunk) {
|
||||||
|
channel.addEventListener('bufferedamountlow', () => {
|
||||||
|
const chunk = getNextChunk();
|
||||||
|
if (chunk) channel.send(chunk);
|
||||||
|
});
|
||||||
|
channel.bufferedAmountLowThreshold = 16e3 - 1;
|
||||||
|
const first = getNextChunk();
|
||||||
|
if (first) channel.send(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
import MWSEEventTarget from './EventTarget.js';
|
||||||
|
import { PeerInfo } from './PeerInfo.js';
|
||||||
|
import WebRTC from './WebRTC.js';
|
||||||
|
|
||||||
|
export default class Peer extends MWSEEventTarget {
|
||||||
|
constructor(mwse) {
|
||||||
|
super();
|
||||||
|
this.mwse = mwse;
|
||||||
|
this.options = {};
|
||||||
|
this.socketId = undefined;
|
||||||
|
this.selfSocket = false;
|
||||||
|
this.active = false;
|
||||||
|
this.peerConnection = false;
|
||||||
|
this.primaryChannel = 'datachannel';
|
||||||
|
|
||||||
|
this.info = new PeerInfo(this);
|
||||||
|
this.rtc = this._createRTC();
|
||||||
|
|
||||||
|
// Route incoming packs: RTC signaling goes to the RTC engine,
|
||||||
|
// everything else surfaces as a 'message' event.
|
||||||
|
this.on('pack', data => {
|
||||||
|
if (data.type === ':rtcpack:') {
|
||||||
|
this.rtc.emit('input', data.payload);
|
||||||
|
} else {
|
||||||
|
this.emit('message', data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_createRTC(rtcConfig, rtcServers) {
|
||||||
|
const rtc = new WebRTC(rtcConfig, rtcServers);
|
||||||
|
rtc.peer = this;
|
||||||
|
|
||||||
|
rtc.on('connected', () => { this.peerConnection = true; });
|
||||||
|
rtc.on('disconnected', () => { this.peerConnection = false; });
|
||||||
|
|
||||||
|
// RTC output → relay opaque `:rtcpack:` to the paired peer via server.
|
||||||
|
rtc.on('output', payload => {
|
||||||
|
this.send({ type: ':rtcpack:', payload });
|
||||||
|
});
|
||||||
|
|
||||||
|
// RTC data-channel message → surface as peer 'pack' event.
|
||||||
|
rtc.on('message', payload => {
|
||||||
|
this.emit('pack', payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rtc = rtc;
|
||||||
|
return rtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// createRTC allows callers to (re)create the RTC object with custom config.
|
||||||
|
createRTC(rtcConfig, rtcServers) {
|
||||||
|
return this._createRTC(rtcConfig, rtcServers);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPeerOptions(options) {
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
this.setSocketId(options);
|
||||||
|
} else {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSocketId(uuid) {
|
||||||
|
this.socketId = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async metadata() {
|
||||||
|
if (this.socketId === 'me') {
|
||||||
|
const result = await this.mwse.EventPooling.request({ type: 'my/socketid' });
|
||||||
|
this.selfSocket = true;
|
||||||
|
this.active = this.active || true;
|
||||||
|
this.socketId = result;
|
||||||
|
this.emit('scope');
|
||||||
|
this.activeScope = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(pack) {
|
||||||
|
if (this.active) {
|
||||||
|
return this.mwse.request(this.socketId, pack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
equalTo(peer) {
|
||||||
|
return this.socketId === peer.socketId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isReachable() {
|
||||||
|
return this.mwse.EventPooling.request({ type: 'is/reachable', to: this.socketId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async enablePairAuth() {
|
||||||
|
await this.mwse.EventPooling.request({ type: 'auth/pair-system', value: 'everybody' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async disablePairAuth() {
|
||||||
|
await this.mwse.EventPooling.request({ type: 'auth/pair-system', value: 'disable' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async enablePairInfo() {
|
||||||
|
await this.mwse.EventPooling.request({ type: 'connection/pairinfo', value: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async disablePairInfo() {
|
||||||
|
await this.mwse.EventPooling.request({ type: 'connection/pairinfo', value: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestPair() {
|
||||||
|
const { message, status } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'request/pair',
|
||||||
|
to: this.socketId
|
||||||
|
});
|
||||||
|
if (message === 'ALREADY-PAIRED' || message === 'ALREADY-REQUESTED') {
|
||||||
|
console.warn('MWSE: already paired or pair requested');
|
||||||
|
}
|
||||||
|
if (status === 'fail') {
|
||||||
|
console.error('MWSE: requestPair failed', status, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async endPair() {
|
||||||
|
await this.mwse.EventPooling.request({ type: 'end/pair', to: this.socketId });
|
||||||
|
this.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptPair() {
|
||||||
|
const { message, status } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'accept/pair',
|
||||||
|
to: this.socketId
|
||||||
|
});
|
||||||
|
if (status === 'fail') {
|
||||||
|
console.error('MWSE: acceptPair failed', status, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectPair() {
|
||||||
|
const { message, status } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'reject/pair',
|
||||||
|
to: this.socketId
|
||||||
|
});
|
||||||
|
if (status === 'fail') {
|
||||||
|
console.error('MWSE: rejectPair failed', status, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPairedList() {
|
||||||
|
const { value } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'pair/list',
|
||||||
|
to: this.socketId
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(pack) {
|
||||||
|
const p2pOpen = this.peerConnection && this.rtc?.active;
|
||||||
|
const serverOpen = this.mwse.server.connected;
|
||||||
|
|
||||||
|
let channel;
|
||||||
|
if (p2pOpen && serverOpen) {
|
||||||
|
channel = this.primaryChannel === 'websocket' ? 'websocket' : 'datachannel';
|
||||||
|
} else if (serverOpen) {
|
||||||
|
channel = 'websocket';
|
||||||
|
} else {
|
||||||
|
channel = 'datachannel';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel === 'websocket') {
|
||||||
|
if (!this.mwse.writable) {
|
||||||
|
console.warn('MWSE: socket is not writable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// WOM — no waiter registered; the engine returns nil for pack/to (#33).
|
||||||
|
this.mwse.EventPooling.only({ type: 'pack/to', pack, to: this.socketId });
|
||||||
|
} else {
|
||||||
|
if (pack.type !== ':rtcpack:') {
|
||||||
|
this.rtc?.sendMessage(pack);
|
||||||
|
} else {
|
||||||
|
console.warn('MWSE: cannot send :rtcpack: over data channel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forget() {
|
||||||
|
this.mwse.peers.delete(this.socketId);
|
||||||
|
this.mwse.pairs.delete(this.socketId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
export class PeerInfo {
|
||||||
|
constructor(peer) {
|
||||||
|
this.peer = peer;
|
||||||
|
this.info = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(name) {
|
||||||
|
const req = name
|
||||||
|
? { type: 'peer/info', peer: this.peer.socketId, name }
|
||||||
|
: { type: 'peer/info', peer: this.peer.socketId };
|
||||||
|
|
||||||
|
const rinfo = await this.peer.mwse.EventPooling.request(req);
|
||||||
|
if (rinfo.status === 'success') {
|
||||||
|
this.info = rinfo.info;
|
||||||
|
} else {
|
||||||
|
console.warn(rinfo.message);
|
||||||
|
}
|
||||||
|
return this.info;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(name, value) {
|
||||||
|
this.info[name] = value;
|
||||||
|
this.peer.mwse.WSTSProtocol.SendOnly({ type: 'auth/info', name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name) {
|
||||||
|
return name ? this.info[name] : this.info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import MWSEEventTarget from './EventTarget.js';
|
||||||
|
import { RoomInfo } from './RoomInfo.js';
|
||||||
|
|
||||||
|
export default class Room extends MWSEEventTarget {
|
||||||
|
constructor(mwse) {
|
||||||
|
super();
|
||||||
|
this.mwse = mwse;
|
||||||
|
this.peers = new Map();
|
||||||
|
this.info = new RoomInfo(this);
|
||||||
|
|
||||||
|
this.options = undefined;
|
||||||
|
this.config = undefined;
|
||||||
|
this.roomId = undefined;
|
||||||
|
this.accessType = undefined;
|
||||||
|
this.joinType = undefined;
|
||||||
|
this.name = undefined;
|
||||||
|
this.owner = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRoomOptions(options) {
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
this.roomId = options;
|
||||||
|
} else {
|
||||||
|
const defaults = {
|
||||||
|
joinType: 'free',
|
||||||
|
ifexistsJoin: true,
|
||||||
|
accessType: 'private',
|
||||||
|
notifyActionInvite: true,
|
||||||
|
notifyActionJoined: true,
|
||||||
|
notifyActionEjected: true,
|
||||||
|
autoFetchInfo: true
|
||||||
|
};
|
||||||
|
this.config = Object.assign(defaults, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRoomId(uuid) {
|
||||||
|
this.roomId = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRoom(roomOptions) {
|
||||||
|
const config = this.config || roomOptions;
|
||||||
|
const result = await this.mwse.EventPooling.request({ type: 'create-room', ...config });
|
||||||
|
|
||||||
|
if (result.status === 'fail') {
|
||||||
|
if (result.message === 'ALREADY-EXISTS' && this.config?.ifexistsJoin) {
|
||||||
|
return this.join();
|
||||||
|
}
|
||||||
|
throw new Error(result.message || result.messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options = { ...this.config, ...result.room };
|
||||||
|
this.roomId = result.room.id;
|
||||||
|
this.mwse.rooms.set(this.roomId, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async join() {
|
||||||
|
const result = await this.mwse.EventPooling.request({
|
||||||
|
type: 'joinroom',
|
||||||
|
name: this.config.name,
|
||||||
|
credential: this.config.credential,
|
||||||
|
autoFetchInfo: this.config.autoFetchInfo || false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status === 'fail') throw new Error(result.message);
|
||||||
|
|
||||||
|
this.options = { ...this.config, ...result.room };
|
||||||
|
if (result.info) this.info.info = result.info;
|
||||||
|
this.roomId = result.room.id;
|
||||||
|
this.mwse.rooms.set(this.roomId, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async eject() {
|
||||||
|
const { type } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'ejectroom',
|
||||||
|
roomId: this.roomId
|
||||||
|
});
|
||||||
|
this.peers.clear();
|
||||||
|
if (type === 'success') this.mwse.rooms.delete(this.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(pack, wom = false, handshake = false) {
|
||||||
|
if (!this.mwse.writable) {
|
||||||
|
console.warn('MWSE: socket is not writable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (handshake) {
|
||||||
|
const { type } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'pack/room', pack, to: this.roomId, wom, handshake
|
||||||
|
});
|
||||||
|
if (type === 'fail') throw new Error('Cannot send message to room');
|
||||||
|
} else {
|
||||||
|
// WOM broadcast — fire-and-forget, no waiter (#33).
|
||||||
|
this.mwse.EventPooling.only({ type: 'pack/room', pack, to: this.roomId, wom, handshake });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPeers(filter, onlyNumber = false) {
|
||||||
|
if (onlyNumber) {
|
||||||
|
const { count } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'room/peer-count',
|
||||||
|
roomId: this.roomId,
|
||||||
|
filter: filter || {}
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, peers } = await this.mwse.EventPooling.request({
|
||||||
|
type: 'room-peers',
|
||||||
|
roomId: this.roomId,
|
||||||
|
filter: filter || {}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === 'fail') throw new Error('Cannot fetch peers from room');
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
for (const peerId of peers) {
|
||||||
|
const peer = this.mwse.peer(peerId, true);
|
||||||
|
result.push(peer);
|
||||||
|
this.peers.set(peerId, peer);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
export class RoomInfo {
|
||||||
|
constructor(room) {
|
||||||
|
this.room = room;
|
||||||
|
this.info = {};
|
||||||
|
// Keep local cache in sync with server-pushed updates.
|
||||||
|
this.room.on('updateinfo', (name, value) => { this.info[name] = value; });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(name) {
|
||||||
|
const req = name
|
||||||
|
? { type: 'room/getinfo', roomId: this.room.roomId, name }
|
||||||
|
: { type: 'room/info', roomId: this.room.roomId };
|
||||||
|
|
||||||
|
const rinfo = await this.room.mwse.EventPooling.request(req);
|
||||||
|
if (rinfo.status === 'success') {
|
||||||
|
this.info = rinfo.value;
|
||||||
|
} else {
|
||||||
|
console.warn(rinfo.message);
|
||||||
|
}
|
||||||
|
return this.info;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(name, value) {
|
||||||
|
this.info[name] = value;
|
||||||
|
this.room.mwse.WSTSProtocol.SendOnly({
|
||||||
|
type: 'room/setinfo',
|
||||||
|
roomId: this.room.roomId,
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name) {
|
||||||
|
return name ? this.info[name] : this.info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
// WSTS protocol layer — sits between the raw WebSocket and the event pool.
|
||||||
|
// Routes incoming frames to either a correlated reply handler (numeric id) or
|
||||||
|
// a named signal handler (string id). Outbound frames are encoded by the codec.
|
||||||
|
|
||||||
|
import { WSTSCodec } from './codec.js';
|
||||||
|
|
||||||
|
export default class WSTSProtocol {
|
||||||
|
constructor(mwse) {
|
||||||
|
this.mwse = mwse;
|
||||||
|
this.codec = new WSTSCodec();
|
||||||
|
this._listen();
|
||||||
|
}
|
||||||
|
|
||||||
|
_listen() {
|
||||||
|
this.mwse.server.onRecaivePack(raw => this.PackAnalyze(raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Outbound -------------------------------------------------------
|
||||||
|
|
||||||
|
// SendOnly: fire-and-forget WOM frame — no id, no pending waiter.
|
||||||
|
SendOnly(pack) {
|
||||||
|
this.mwse.server.tranferToServer(this.codec.encode([pack, 'R']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRequest: request expecting exactly one reply (action 'R' → server replies 'E').
|
||||||
|
SendRequest(pack, id) {
|
||||||
|
this.mwse.server.tranferToServer(this.codec.encode([pack, id, 'R']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartStream: request expecting multiple replies (server replies 'C' until done).
|
||||||
|
StartStream(pack, id) {
|
||||||
|
this.mwse.server.tranferToServer(this.codec.encode([pack, id, 'S']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRaw: bypass codec (used by internal tooling only).
|
||||||
|
SendRaw(pack) {
|
||||||
|
this.mwse.server.tranferToServer(JSON.stringify(pack));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Inbound --------------------------------------------------------
|
||||||
|
|
||||||
|
// PackAnalyze routes one decoded frame to the right listener.
|
||||||
|
// raw may be a string (JSON) or an ArrayBuffer (binary, when #42 lands).
|
||||||
|
PackAnalyze(raw) {
|
||||||
|
const data = this.codec.decode(raw);
|
||||||
|
const [payload, id, action] = data;
|
||||||
|
|
||||||
|
if (typeof id === 'number') {
|
||||||
|
// Correlated reply: numeric id maps to a pending request.
|
||||||
|
const entry = this.mwse.EventPooling.events.get(id);
|
||||||
|
if (entry) {
|
||||||
|
entry[0](payload, action);
|
||||||
|
if (action === 'E') {
|
||||||
|
// 'E'nd — one-shot request fulfilled; remove the waiter.
|
||||||
|
this.mwse.EventPooling.events.delete(id);
|
||||||
|
}
|
||||||
|
// 'C'ontinue — stream replies stay in the map until the server closes.
|
||||||
|
} else {
|
||||||
|
console.warn('MWSE: reply for unknown request id', id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Signal: string id (signal name) fans out to all registered listeners.
|
||||||
|
const listeners = this.mwse.EventPooling.signals.get(id);
|
||||||
|
if (listeners) {
|
||||||
|
for (const cb of listeners) cb(payload);
|
||||||
|
} else {
|
||||||
|
console.warn('MWSE: unhandled signal', id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
// WebRTC.js — placeholder. Full WebRTC implementation will be rewritten separately.
|
||||||
|
// Provides the interface Peer.js depends on so the rest of the SDK loads cleanly.
|
||||||
|
import MWSEEventTarget from './EventTarget.js';
|
||||||
|
|
||||||
|
export default class WebRTC extends MWSEEventTarget {
|
||||||
|
constructor(_rtcConfig, _rtcServers) {
|
||||||
|
super();
|
||||||
|
this.active = false;
|
||||||
|
this.peer = null;
|
||||||
|
this.connectionStatus = 'new';
|
||||||
|
this.iceStatus = 'new';
|
||||||
|
this.gatheringStatus = 'new';
|
||||||
|
this.signalingStatus = '';
|
||||||
|
this.channel = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
console.warn('WebRTC: not yet implemented — replacement coming separately');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStream(_stream, _name, _info) {
|
||||||
|
console.warn('WebRTC.sendStream: not yet implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(_data) {
|
||||||
|
console.warn('WebRTC.sendMessage: not yet implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.active = false;
|
||||||
|
this.emit('disconnected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// send is called internally by the signaling output path in the full implementation.
|
||||||
|
send(_data) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
// WSTS codec — encoder/decoder for the WebSocket frames MWSE exchanges.
|
||||||
|
//
|
||||||
|
// Infrastructure for issue #42 (binary framing). The codec is pluggable: today
|
||||||
|
// it speaks JSON text frames (mode=CODEC_JSON); the binary path (mode=CODEC_BINARY)
|
||||||
|
// is stubbed and will be wired once the server side lands.
|
||||||
|
//
|
||||||
|
// Frame shapes (codec-agnostic, defined by WSTS protocol):
|
||||||
|
// Inbound from server: [payload, signalName] — signal
|
||||||
|
// [payload, id, 'E'|'C'] — reply
|
||||||
|
// Outbound to server: [msg, 'R'] — fire-and-forget (WOM)
|
||||||
|
// [msg, id, 'R'] — request (reply once)
|
||||||
|
// [msg, id, 'S'] — stream (reply multiple)
|
||||||
|
import { CODEC_JSON, CODEC_BINARY } from './version.js';
|
||||||
|
|
||||||
|
export class WSTSCodec {
|
||||||
|
constructor() {
|
||||||
|
this.mode = CODEC_JSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode turns a JS frame array into a wire value (string or ArrayBuffer).
|
||||||
|
encode(frame) {
|
||||||
|
if (this.mode === CODEC_JSON) {
|
||||||
|
return JSON.stringify(frame);
|
||||||
|
}
|
||||||
|
if (this.mode === CODEC_BINARY) {
|
||||||
|
return this._encodeBinary(frame);
|
||||||
|
}
|
||||||
|
throw new Error(`WSTSCodec: unknown mode ${this.mode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode turns raw wire data into a JS frame array.
|
||||||
|
decode(raw) {
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
}
|
||||||
|
if (raw instanceof ArrayBuffer) {
|
||||||
|
if (this.mode === CODEC_BINARY) {
|
||||||
|
return this._decodeBinary(raw);
|
||||||
|
}
|
||||||
|
throw new Error('WSTSCodec: received binary frame but codec is in JSON mode');
|
||||||
|
}
|
||||||
|
throw new Error(`WSTSCodec: unexpected frame type ${typeof raw}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// negotiate picks the best codec from the server's offered list and activates it.
|
||||||
|
// Returns true when a common codec was found.
|
||||||
|
negotiate(serverCodecs) {
|
||||||
|
// Prefer binary when both sides support it (future upgrade path).
|
||||||
|
if (serverCodecs.includes(CODEC_BINARY)) {
|
||||||
|
this.mode = CODEC_BINARY;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (serverCodecs.includes(CODEC_JSON)) {
|
||||||
|
this.mode = CODEC_JSON;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Binary codec (issue #42) ----------------------------------------
|
||||||
|
// Wire layout: 1-byte opcode | 4-byte uint32 big-endian payload length | payload bytes
|
||||||
|
// Opcodes: 0x01 = signal, 0x02 = reply-end ('E'), 0x03 = reply-continue ('C'),
|
||||||
|
// 0x04 = request ('R'), 0x05 = stream ('S'), 0x06 = wom ('R' no id)
|
||||||
|
// Payload: MessagePack (or CBOR) encoded frame contents.
|
||||||
|
// This is intentionally NOT implemented yet — throws so callers know when they
|
||||||
|
// accidentally reach binary paths before #42 is merged.
|
||||||
|
|
||||||
|
_encodeBinary(_frame) {
|
||||||
|
throw new Error('WSTSCodec binary encoding not yet implemented (#42)');
|
||||||
|
}
|
||||||
|
|
||||||
|
_decodeBinary(_buf) {
|
||||||
|
throw new Error('WSTSCodec binary decoding not yet implemented (#42)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
// MWSE SDK — ES module entry point.
|
||||||
|
//
|
||||||
|
// Load via:
|
||||||
|
// <script type="module" src="https://ws.example.com/sdk/index.js"></script>
|
||||||
|
// or through the /sdk.js redirect:
|
||||||
|
// <script type="module" src="https://ws.example.com/sdk.js"></script>
|
||||||
|
//
|
||||||
|
// Because this is an ES module, all imports below resolve relative to
|
||||||
|
// import.meta.url (= the URL of this file on the MWSE server). Every other SDK
|
||||||
|
// file therefore loads from the same origin automatically — no bundler needed.
|
||||||
|
//
|
||||||
|
// Version handshake:
|
||||||
|
// On connect the server sends a wsts/hello signal carrying its version string.
|
||||||
|
// If it does not match SDK_VERSION the SDK fires an 'error' event, closes the
|
||||||
|
// connection, and never fires 'scope'. This prevents accidental use of an SDK
|
||||||
|
// against an incompatible engine.
|
||||||
|
|
||||||
|
import { SDK_VERSION } from './version.js';
|
||||||
|
import { Connection } from './Connection.js';
|
||||||
|
import WSTSProtocol from './WSTSProtocol.js';
|
||||||
|
import EventPool from './EventPool.js';
|
||||||
|
import { IPPressure } from './IPPressure.js';
|
||||||
|
import Peer from './Peer.js';
|
||||||
|
import Room from './Room.js';
|
||||||
|
|
||||||
|
export default class MWSE {
|
||||||
|
constructor(options) {
|
||||||
|
this.rooms = new Map();
|
||||||
|
this.pairs = new Map();
|
||||||
|
this.peers = new Map();
|
||||||
|
|
||||||
|
this.writable = 1;
|
||||||
|
this.readable = 1;
|
||||||
|
|
||||||
|
this._events = {};
|
||||||
|
this.activeScope = false;
|
||||||
|
|
||||||
|
this.server = new Connection(this, options);
|
||||||
|
this.WSTSProtocol = new WSTSProtocol(this);
|
||||||
|
this.EventPooling = new EventPool(this);
|
||||||
|
this.virtualPressure = new IPPressure(this);
|
||||||
|
|
||||||
|
this.me = new Peer(this);
|
||||||
|
this.me.scope(() => {
|
||||||
|
this.peers.set('me', this.me);
|
||||||
|
this.peers.set(this.me.socketId, this.me);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._wireSignals();
|
||||||
|
|
||||||
|
this.server.connect();
|
||||||
|
|
||||||
|
// Version handshake happens before scope. onActive waits for wsts/hello;
|
||||||
|
// only on success does it fire the user's scope callbacks.
|
||||||
|
this.server.onActive(async () => {
|
||||||
|
try {
|
||||||
|
await this._awaitHello();
|
||||||
|
} catch (err) {
|
||||||
|
this.emit('error', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.me.setSocketId('me');
|
||||||
|
await this.me.metadata();
|
||||||
|
this.emit('scope');
|
||||||
|
this.activeScope = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.onPassive(() => {
|
||||||
|
this.emit('close');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Version handshake ----------------------------------------------
|
||||||
|
|
||||||
|
// _awaitHello waits for the server's wsts/hello signal and validates the
|
||||||
|
// version. Resolves on success; rejects (and closes the connection) on
|
||||||
|
// mismatch or timeout.
|
||||||
|
_awaitHello() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error('MWSE: wsts/hello timeout — server did not send a version handshake'));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.EventPooling.signal('wsts/hello', ({ v, codecs }) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
|
||||||
|
if (v !== SDK_VERSION) {
|
||||||
|
this.server.disconnect();
|
||||||
|
reject(new Error(
|
||||||
|
`MWSE version mismatch — server: ${v}, SDK: ${SDK_VERSION}. ` +
|
||||||
|
'Update both to the same version.'
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negotiate the best codec both sides support.
|
||||||
|
this.WSTSProtocol.codec.negotiate(codecs || [0]);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Event emitter (base, inlined) ----------------------------------
|
||||||
|
|
||||||
|
on(eventName, callback) {
|
||||||
|
(this._events[eventName] ??= []).push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(eventName, ...args) {
|
||||||
|
for (const cb of (this._events[eventName] || [])) cb(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// scope(f) fires f now if already in scope, otherwise on the next 'scope' event.
|
||||||
|
scope(f) {
|
||||||
|
if (this.activeScope) f();
|
||||||
|
else this.on('scope', f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Peer helpers ---------------------------------------------------
|
||||||
|
|
||||||
|
room(options) {
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
if (this.rooms.has(options)) return this.rooms.get(options);
|
||||||
|
}
|
||||||
|
const r = new Room(this);
|
||||||
|
r.setRoomOptions(options);
|
||||||
|
this.emit('room');
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
peer(options, isActive = false) {
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
if (this.peers.has(options)) return this.peers.get(options);
|
||||||
|
if (this.pairs.has(options)) return this.pairs.get(options);
|
||||||
|
}
|
||||||
|
const p = new Peer(this);
|
||||||
|
p.setPeerOptions(options);
|
||||||
|
p.active = isActive;
|
||||||
|
this.peers.set(p.socketId, p);
|
||||||
|
this.emit('peer', p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(peerId, pack) {
|
||||||
|
const { pack: answer } = await this.EventPooling.request({
|
||||||
|
type: 'request/to',
|
||||||
|
to: peerId,
|
||||||
|
pack
|
||||||
|
});
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async response(peerId, requestId, pack) {
|
||||||
|
this.WSTSProtocol.SendOnly({ type: 'response/to', to: peerId, pack, id: requestId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Session flags --------------------------------------------------
|
||||||
|
|
||||||
|
enableRecaiveData() { this.WSTSProtocol.SendOnly({ type: 'connection/packrecaive', value: 1 }); this.readable = 1; }
|
||||||
|
disableRecaiveData() { this.WSTSProtocol.SendOnly({ type: 'connection/packrecaive', value: 0 }); this.readable = 0; }
|
||||||
|
enableSendData() { this.WSTSProtocol.SendOnly({ type: 'connection/packsending', value: 1 }); this.writable = 1; }
|
||||||
|
disableSendData() { this.WSTSProtocol.SendOnly({ type: 'connection/packsending', value: 0 }); this.writable = 0; }
|
||||||
|
enableNotifyRoomInfo() { this.WSTSProtocol.SendOnly({ type: 'connection/roominfo', value: 1 }); }
|
||||||
|
disableNotifyRoomInfo() { this.WSTSProtocol.SendOnly({ type: 'connection/roominfo', value: 0 }); }
|
||||||
|
|
||||||
|
destroy() { this.server.disconnect(); }
|
||||||
|
|
||||||
|
// ---- Signal wiring --------------------------------------------------
|
||||||
|
|
||||||
|
_wireSignals() {
|
||||||
|
const ep = this.EventPooling;
|
||||||
|
|
||||||
|
ep.signal('pack', ({ from, pack }) => {
|
||||||
|
if (this.readable) {
|
||||||
|
this.peer(from, true).emit('pack', pack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('request', ({ from, pack, id }) => {
|
||||||
|
const scope = {
|
||||||
|
body: pack,
|
||||||
|
response: (replyPack) => this.response(from, id, replyPack),
|
||||||
|
peer: this.peer(from, true)
|
||||||
|
};
|
||||||
|
this.peer(from, true).emit('request', scope);
|
||||||
|
this.peer('me').emit('request', scope);
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('pack/room', ({ from, pack, sender }) => {
|
||||||
|
if (this.readable) {
|
||||||
|
this.room(from).emit('message', pack, this.peer(sender));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('room/joined', ({ id, roomid }) => {
|
||||||
|
const room = this.room(roomid);
|
||||||
|
const peer = this.peer(id, true);
|
||||||
|
room.peers.set(peer.socketId, peer);
|
||||||
|
room.emit('join', peer);
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('room/info', ({ roomId, name, value }) => {
|
||||||
|
this.room(roomId).emit('updateinfo', name, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('room/ejected', ({ id, roomid }) => {
|
||||||
|
const room = this.room(roomid);
|
||||||
|
const peer = this.peer(id, true);
|
||||||
|
room.peers.delete(peer.socketId);
|
||||||
|
room.emit('eject', peer);
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('room/closed', ({ roomid }) => {
|
||||||
|
const room = this.room(roomid);
|
||||||
|
room.peers.clear();
|
||||||
|
room.emit('close');
|
||||||
|
this.rooms.delete(roomid);
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('pair/info', ({ from, name, value }) => {
|
||||||
|
this.peer(from, true).info.info[name] = value;
|
||||||
|
this.peer(from, true).emit('info', name, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('request/pair', ({ from, info }) => {
|
||||||
|
const peer = this.peer(from, true);
|
||||||
|
peer.info.info = info;
|
||||||
|
peer.emit('request/pair', peer);
|
||||||
|
this.peer('me').emit('request/pair', peer);
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('peer/disconnect', ({ id }) => {
|
||||||
|
this.peer(id, true).emit('disconnect');
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('accepted/pair', ({ from, info }) => {
|
||||||
|
const peer = this.peer(from, true);
|
||||||
|
peer.info.info = info;
|
||||||
|
peer.emit('accepted/pair', peer);
|
||||||
|
this.peer('me').emit('accepted/pair', peer);
|
||||||
|
});
|
||||||
|
|
||||||
|
ep.signal('end/pair', ({ from, info }) => {
|
||||||
|
const peer = this.peer(from, true);
|
||||||
|
peer.emit('end/pair', info);
|
||||||
|
this.peer('me').emit('end/pair', from, info);
|
||||||
|
});
|
||||||
|
|
||||||
|
// server/pack — message pushed by the application server via /api/client/:id/send
|
||||||
|
ep.signal('server/pack', ({ from, fromServer, pack }) => {
|
||||||
|
if (this.readable) {
|
||||||
|
this.emit('server/pack', { from, fromServer, pack });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose on window for non-module usage patterns (e.g. inline <script> that
|
||||||
|
// refers to MWSE after the module has loaded).
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.MWSE = MWSE;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
// SDK wire protocol version — must match internal/protocol/version.go WSTSVersion.
|
||||||
|
// Both sides refuse to operate when versions differ.
|
||||||
|
export const SDK_VERSION = '1.0.0';
|
||||||
|
|
||||||
|
// Codec identifiers (see codec.js). The server advertises supported codecs in the
|
||||||
|
// wsts/hello signal; the client picks the highest it supports.
|
||||||
|
export const CODEC_JSON = 0; // JSON text frames — current
|
||||||
|
export const CODEC_BINARY = 1; // Binary frames — planned (#42)
|
||||||
Loading…
Reference in New Issue