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}