code wiki / (root) / nx_chat_ring.nx

nx_chat_ring.nx source

↩ module page · 190 lines · 6467 B

1// nx_chat_ring.nx -- bounded-ring chat buffer with sealed message slots. 2// 3// Promoted from the nishi_video_room_synology.nx L77-L78 chat ring 4// (per [[project-nishifamily-video-l66-l75-2026-05-16]] commits 8406e7a9 5// + 846fd399). Same wire format + same persistence semantics; new 6// home is the OOP layer so any consumer (video room, nx_call, 7// nx_cli_meet, nx_iot_intercom) can reuse it without re-implementing. 8// 9// Ring contract: 10// - Sealed capacity at construction; full ring wraps oldest-first. 11// - Monotonic sequence numbers (consumers can ask "what's new since N?"). 12// - Per-message metadata: seq, sender_peer_id, kind, byte_len, unix_ms. 13// - Pluggable persistence (disk / memory / future MLS-encrypted). 14 15// nx_safety_envelope: 16// intended_use: reusable chat ring for sovereign media stack 17// sil_target: SIL2 18// evidence: composes shipped video-room chat semantics; 19// bench/nx_chat_ring_smoke.nx asserts wrap + since-N 20// verdict: NOT_YET_EVALUATED -- pending smoke 21 22import "nx_syscalls_x86_64.nx" 23 24// ---- Sealed message kinds ---- 25// 26// Append-only. Wire-stable. Wire-incompatible reuse forbidden. 27 28const NX_CHAT_KIND_UNKNOWN: i64 = 0 29const NX_CHAT_KIND_TEXT: i64 = 1 // plain user text 30const NX_CHAT_KIND_REACT: i64 = 2 // emoji/reaction 31const NX_CHAT_KIND_PRESENCE: i64 = 3 // "alice joined / alice left" 32const NX_CHAT_KIND_SYS: i64 = 4 // server status notice 33const NX_CHAT_KIND_NAME: i64 = 5 // peer set display name 34const NX_CHAT_KIND_HANDRAISE: i64 = 6 35const NX_CHAT_KIND_N: i64 = 7 36 37// ---- Sealed verdicts ---- 38 39const NX_CHAT_RING_OK: i64 = 0 40const NX_CHAT_RING_ERR_FULL_TXT: i64 = -1 // body too big for ring text-arena 41const NX_CHAT_RING_ERR_BAD_KIND: i64 = -2 42const NX_CHAT_RING_ERR_BAD_SEQ: i64 = -3 43const NX_CHAT_RING_ERR_TXT_CAP: i64 = -4 // requested single msg larger than ring text capacity 44 45// ---- Slot metadata ---- 46// 47// Fixed-size record per message; the message text/body lives in a 48// separate byte arena and is identified by offset+length. 49// Slot bytes: 6 i64 = 48. 50 51struct ChatSlot { 52 seq: i64, // monotonic; 0 means "empty" 53 peer_id: i64, // sender (0 = system) 54 kind: i64, // NX_CHAT_KIND_* 55 txt_off: i64, // byte offset into the arena 56 txt_len: i64, // bytes 57 unix_ms: i64, 58} 59 60const NX_CHAT_SLOT_BYTES: i64 = 48 61 62// ---- Ring container ---- 63 64struct nx_chat_ring { 65 slots: *ChatSlot, // capacity slots 66 cap_slots: i64, 67 head: i64, // index of next-write slot (mod cap_slots) 68 full: i64, // 0 until first wrap; 1 once we've wrapped at least once 69 next_seq: i64, // next seq number to assign 70 txt_arena: *u8, 71 txt_cap: i64, 72 txt_head: i64, // byte offset of next-write in txt_arena (wraps) 73} 74 75const NX_CHAT_RING_BYTES: i64 = 64 76 77// ---- Construction ---- 78 79func nx_chat_ring_new(r: *nx_chat_ring, cap_slots: i64, txt_cap: i64) -> i64 { 80 let bytes_for_slots: i64 = cap_slots * NX_CHAT_SLOT_BYTES 81 let slots: *ChatSlot = sys_mmap(bytes_for_slots) as *ChatSlot 82 let arena: *u8 = sys_mmap(txt_cap) 83 r.slots = slots 84 r.cap_slots = cap_slots 85 r.head = 0 86 r.full = 0 87 r.next_seq = 1 88 r.txt_arena = arena 89 r.txt_cap = txt_cap 90 r.txt_head = 0 91 // Zero all seq fields so 0 means "empty". 92 var i: i64 = 0 93 while i < cap_slots { 94 let s: *ChatSlot = (slots as i64 + i * NX_CHAT_SLOT_BYTES) as *ChatSlot 95 s.seq = 0 96 i = i + 1 97 } 98 return NX_CHAT_RING_OK 99} 100 101// ---- Append a message ---- 102 103func nx_chat_ring_append(r: *nx_chat_ring, 104 peer_id: i64, kind: i64, 105 txt: *u8, txt_len: i64, 106 unix_ms: i64) -> i64 { 107 if kind <= NX_CHAT_KIND_UNKNOWN { return NX_CHAT_RING_ERR_BAD_KIND } 108 if kind >= NX_CHAT_KIND_N { return NX_CHAT_RING_ERR_BAD_KIND } 109 if txt_len > r.txt_cap { return NX_CHAT_RING_ERR_TXT_CAP } 110 111 // Place text in arena, wrapping if necessary. 112 if r.txt_head + txt_len > r.txt_cap { 113 r.txt_head = 0 114 } 115 let txt_off: i64 = r.txt_head 116 var i: i64 = 0 117 while i < txt_len { 118 r.txt_arena[txt_off + i] = txt[i] 119 i = i + 1 120 } 121 r.txt_head = r.txt_head + txt_len 122 123 // Write slot. 124 let slot: *ChatSlot = (r.slots as i64 + r.head * NX_CHAT_SLOT_BYTES) as *ChatSlot 125 let assigned_seq: i64 = r.next_seq 126 slot.seq = assigned_seq 127 slot.peer_id = peer_id 128 slot.kind = kind 129 slot.txt_off = txt_off 130 slot.txt_len = txt_len 131 slot.unix_ms = unix_ms 132 133 r.next_seq = assigned_seq + 1 134 r.head = r.head + 1 135 if r.head >= r.cap_slots { 136 r.head = 0 137 r.full = 1 138 } 139 return assigned_seq 140} 141 142// ---- Accessors ---- 143 144func nx_chat_ring_next_seq(r: *nx_chat_ring) -> i64 { 145 return r.next_seq 146} 147 148func nx_chat_ring_count(r: *nx_chat_ring) -> i64 { 149 if r.full == 1 { return r.cap_slots } 150 return r.head 151} 152 153// Resolve a sequence number to a slot pointer (or 0 if not present). 154// Walks the ring; cap_slots small so linear scan is fine. 155func nx_chat_ring_get_by_seq(r: *nx_chat_ring, seq: i64) -> *ChatSlot { 156 if seq <= 0 { return 0 as *ChatSlot } 157 var i: i64 = 0 158 while i < r.cap_slots { 159 let s: *ChatSlot = (r.slots as i64 + i * NX_CHAT_SLOT_BYTES) as *ChatSlot 160 if s.seq == seq { return s } 161 i = i + 1 162 } 163 return 0 as *ChatSlot 164} 165 166// Get the i'th slot in ring order (0 = oldest still-resident). 167func nx_chat_ring_get_at(r: *nx_chat_ring, i: i64) -> *ChatSlot { 168 let count: i64 = nx_chat_ring_count(r) 169 if i < 0 { return 0 as *ChatSlot } 170 if i >= count { return 0 as *ChatSlot } 171 // Oldest = r.head - count when not wrapped, else r.head. 172 var start: i64 = 0 173 if r.full == 1 { start = r.head } 174 let idx: i64 = (start + i) % r.cap_slots 175 return (r.slots as i64 + idx * NX_CHAT_SLOT_BYTES) as *ChatSlot 176} 177 178// Copy a slot's text into the caller-provided buffer. Returns bytes 179// copied or -1 on bad input. 180func nx_chat_ring_copy_text(r: *nx_chat_ring, slot: *ChatSlot, 181 dst: *u8, dst_cap: i64) -> i64 { 182 if slot as i64 == 0 { return -1 } 183 if slot.txt_len > dst_cap { return -1 } 184 var i: i64 = 0 185 while i < slot.txt_len { 186 dst[i] = r.txt_arena[slot.txt_off + i] 187 i = i + 1 188 } 189 return slot.txt_len 190}