feat: optional per-room JetStream persistence (history + offline replay), gated by RoomPolicy.Persist
This commit is contained in:
+10
-2
@@ -166,6 +166,13 @@
|
||||
<input id="roomEncrypt" type="checkbox" />
|
||||
<label for="roomEncrypt">🔒 encrypted (E2E)</label>
|
||||
</div>
|
||||
<div class="checkrow">
|
||||
<input id="roomPersist" type="checkbox" />
|
||||
<label for="roomPersist">🗂 persistente (historial)</label>
|
||||
</div>
|
||||
<div class="help" style="margin:-4px 0 8px; font-size:12px; color:var(--muted)">
|
||||
persistente = quien se une despues ve el historial; sin persistir = solo mensajes nuevos (NATS simple).
|
||||
</div>
|
||||
<button id="createRoomBtn" disabled>Create room</button>
|
||||
<div style="border-top:1px solid var(--border); margin:12px 0"></div>
|
||||
<label>Join by room_id</label>
|
||||
@@ -370,16 +377,17 @@ $("connectBtn").onclick = async () => {
|
||||
$("createRoomBtn").onclick = async () => {
|
||||
const subject = $("roomSubject").value.trim();
|
||||
const encrypt = $("roomEncrypt").checked;
|
||||
const persist = $("roomPersist").checked;
|
||||
if (!subject) { setStatus("roomStatus", "subject required", "bad"); return; }
|
||||
try {
|
||||
const res = await api("/api/room", { peer: state.peer, subject, encrypt });
|
||||
const res = await api("/api/room", { peer: state.peer, subject, encrypt, persist });
|
||||
state.rooms[res.room_id] = { subject: res.subject, encrypt: res.encrypt };
|
||||
refreshRoomSelect();
|
||||
$("activeRoom").value = res.room_id;
|
||||
setStatus("roomStatus", "created " + res.room_id + " (click to copy)", "ok");
|
||||
$("roomStatus").style.cursor = "pointer";
|
||||
$("roomStatus").onclick = () => navigator.clipboard.writeText(res.room_id);
|
||||
logSys("created room " + res.subject + " [" + short(res.room_id) + "]" + (encrypt ? " 🔒" : ""));
|
||||
logSys("created room " + res.subject + " [" + short(res.room_id) + "]" + (encrypt ? " 🔒" : "") + (res.persist ? " 🗄" : ""));
|
||||
} catch (e) {
|
||||
setStatus("roomStatus", e.message, "bad");
|
||||
}
|
||||
|
||||
+10
-11
@@ -34,8 +34,6 @@ import (
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/enmanuel/unibus/pkg/blobstore"
|
||||
"github.com/enmanuel/unibus/pkg/client"
|
||||
"github.com/enmanuel/unibus/pkg/embeddednats"
|
||||
@@ -86,9 +84,9 @@ type peerState struct {
|
||||
endpoint client.Endpoint
|
||||
|
||||
mu sync.Mutex
|
||||
subs map[string]*nats.Subscription // roomID -> subscription
|
||||
rooms map[string]roomInfo // roomID -> subject/encrypt
|
||||
listeners map[chan Event]struct{} // attached SSE channels
|
||||
subs map[string]*client.Sub // roomID -> subscription
|
||||
rooms map[string]roomInfo // roomID -> subject/encrypt
|
||||
listeners map[chan Event]struct{} // attached SSE channels
|
||||
}
|
||||
|
||||
// emit fans an event out to all attached listeners without blocking on a slow
|
||||
@@ -155,7 +153,7 @@ func (h *Hub) getOrCreate(name string) (*peerState, error) {
|
||||
name: name,
|
||||
client: c,
|
||||
endpoint: c.Endpoint(),
|
||||
subs: map[string]*nats.Subscription{},
|
||||
subs: map[string]*client.Sub{},
|
||||
rooms: map[string]roomInfo{},
|
||||
listeners: map[chan Event]struct{}{},
|
||||
}
|
||||
@@ -311,6 +309,7 @@ func (h *Hub) handleRoom(w http.ResponseWriter, r *http.Request) {
|
||||
Peer string `json:"peer"`
|
||||
Subject string `json:"subject"`
|
||||
Encrypt bool `json:"encrypt"`
|
||||
Persist bool `json:"persist"`
|
||||
}
|
||||
if err := decodeBody(r, &req); err != nil || req.Peer == "" || req.Subject == "" {
|
||||
writeErr(w, http.StatusBadRequest, "peer and subject required")
|
||||
@@ -321,10 +320,10 @@ func (h *Hub) handleRoom(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, http.StatusBadRequest, "unknown peer "+req.Peer)
|
||||
return
|
||||
}
|
||||
policy := room.ModeNATS
|
||||
if req.Encrypt {
|
||||
policy = room.ModeMatrix
|
||||
}
|
||||
// The two checkboxes map to an explicit per-room policy. encrypt drives both
|
||||
// encryption and per-message signing; persist (default false) independently
|
||||
// toggles durable JetStream history. persist=false keeps plain ephemeral NATS.
|
||||
policy := room.Policy{Encrypt: req.Encrypt, Persist: req.Persist, SignMsgs: req.Encrypt}
|
||||
roomID, err := p.client.CreateRoom(req.Subject, policy)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -336,7 +335,7 @@ func (h *Hub) handleRoom(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"room_id": roomID, "subject": req.Subject, "encrypt": req.Encrypt,
|
||||
"room_id": roomID, "subject": req.Subject, "encrypt": req.Encrypt, "persist": req.Persist,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user