code wiki / _hdl_build / nx_coedit.nx

nx_coedit.nx source

↩ module page · 780 lines · 33873 B

1// nx_coedit.nx -- COLLABORATIVE-EDITING KERNEL (office census OF-C1's engine half): an RGA text CRDT. 2// Multiple replicas edit the SAME document concurrently; their op logs MERGE deterministically -- the same 3// final text on every replica REGARDLESS of merge order (convergence), no lost updates, tombstoned deletes, 4// deterministic (counter,replica) tie-break for concurrent inserts at the same point. 5// 6// OP LOG (append-only, LF lines; the natural fit for the additive-only store doctrine): 7// I <replica> <counter> <afterid> <char> insert ONE byte after node <afterid> ("R" = document root) 8// D <replica> <counter> <targetid> tombstone node <targetid> 9// ids are "<replica>:<counter>"; an op's id must be UNIQUE (same id + different content = LOUD protocol 10// violation; identical duplicate = idempotent skip, which is what makes re-merging safe). 11// <char> is one byte, escaped: \n=newline \t=tab \s=space \\=backslash (any other 1 byte literal). 12// 13// COMMANDS 14// apply <log> <out.txt> materialize (RGA linearization) -> file + verdict line 15// ins <log> <rep> <ctr> <afterid> <char> append a raw insert op 16// del <log> <rep> <ctr> <targetid> append a raw delete op 17// merge <A> <B> <out> union by op id (idempotent dedup; conflicting id = LOUD) 18// edit <log> <rep> <pos> ins <text> position-based convenience: per-BYTE ops chained after the 19// edit <log> <rep> <pos> del <count> visible byte at pos-1 / tombstones for [pos,pos+count) 20// 21// HONEST SCOPE (v1, named): the KERNEL -- byte-granular ops (UTF-8 sequences survive as consecutive ops but 22// byte-position edits must not split a sequence), logs are plain files, ops cap 65536/doc. LIVE multi-client 23// sync (the R2 daemon + browser surface) is NOT built yet; the census OF-C1 axis stays ABSENT until R2 -- 24// its needle requires the R2 sync function DEFINITION, which this file intentionally does not contain. 25// license_tier: ORIGINAL 26import "nx_syscalls.nx" 27const CD_MAGIC_1000003: i64 = 1000003 28const CD_MAGIC_10007: i64 = 10007 29const CD_MAGIC_12289: i64 = 12289 30const CD_MAGIC_4194320: i64 = 4194320 31const CD_MAGIC_4194304: i64 = 4194304 32const CD_MAGIC_4294967296: i64 = 4294967296 33const CD_MAGIC_8388640: i64 = 8388640 34const CD_MAGIC_8388608: i64 = 8388608 35const CD_MAGIC_8388600: i64 = 8388600 36 37const CD_MAXOPS: i64 = 65536 38const CD_HASHN: i64 = 262144 // open-addressing id -> node-idx+1 (power of two) 39 40func cd_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8) { n=n+1 } return n } 41func cd_puts(s: *u8) -> i64 { sys_write(1, s, cd_slen(s)); return 0 } 42func cd_num(v: i64) -> i64 { 43 let b: *u8=sys_mmap(28); var m: i64=v; if m<0 { m=0-m; sys_write(1,"-" as *u8,1) } 44 let t: *u8=sys_mmap(28); var k: i64=0; if m==0 { t[0]=48 as u8; k=1 } 45 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 } 46 var i: i64=0; while i<k { b[i]=t[k-1-i]; i=i+1 } sys_write(1,b,k); return 0 47} 48func cd_streq(a: *u8, b: *u8) -> i64 { var i: i64=0; while a[i]!=(0 as u8) { if a[i]!=b[i] { return 0 } i=i+1 } if b[i]!=(0 as u8) { return 0 } return 1 } 49func cd_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8) { dst[off+i]=s[i]; i=i+1 } return off+i } 50func cd_uint(dst: *u8, off: i64, v: i64) -> i64 { 51 var m: i64=v; let t: *u8=sys_mmap(28); var k: i64=0; if m==0 { t[0]=48 as u8; k=1 } 52 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 } 53 var o: i64=off; var i: i64=0; while i<k { dst[o]=t[k-1-i]; o=o+1; i=i+1 } return o 54} 55func cd_readfile(path: *u8, buf: *u8, cap: i64) -> i64 { 56 let fd: i64=sys_openat_rd(path); if fd<0 { return 0-1 } 57 var tot: i64=0; while tot<cap { let r: i64=sys_read(fd,(buf as i64+tot) as *u8,cap-tot); if r<=0 { break } tot=tot+r } 58 sys_close(fd); return tot 59} 60func cd_appendfile(path: *u8, line: *u8, n: i64) -> i64 { 61 let fd: i64=sys_openat_append(path, 0x1a4); if fd<0 { return 0-1 } 62 sys_write(fd, line, n); sys_close(fd); return 0 63} 64func cd_writefile(path: *u8, buf: *u8, n: i64) -> i64 { 65 let fd: i64=sys_openat_wr(path, 0x1a4); if fd<0 { return 0-1 } 66 sys_write(fd, buf, n); sys_close(fd); return 0 67} 68 69// parse an unsigned int at s[*ip..e); stops at non-digit; -1 if no digits 70func cd_int(s: *u8, ip: *i64, e: i64) -> i64 { 71 var i: i64=ip[0] 72 var v: i64=0 73 var nd: i64=0 74 while i<e { let c: i64=s[i] as i64; if c>=48 { if c<=57 { v=v*10+(c-48); nd=nd+1; i=i+1 } else { break } } else { break } } 75 if nd==0 { return 0-1 } 76 ip[0]=i 77 return v 78} 79 80// ---- the replica state: parallel node arrays + an id hash + per-parent child lists (sorted DESC by id) ---- 81struct CdDoc { 82 nrep: *i64, // node replica id 83 nctr: *i64, // node counter 84 nch: *u8, // node byte 85 ntomb: *u8, // tombstone flag 86 cfirst: *i64, // per node: first child idx (or 0 = none; node 0 = ROOT sentinel) 87 cnext: *i64, // per node: next sibling idx (or 0) 88 hsh: *i64, // id hash -> node idx+1 89 n: i64, // node count (incl ROOT at 0) 90 err: i64 // 0 ok | 1 malformed | 2 bad-ref | 3 conflicting-duplicate | 4 cap 91} 92 93func cd_new() -> *CdDoc { 94 let d: *CdDoc = sys_mmap(96) as *CdDoc 95 d.nrep = sys_mmap(8*CD_MAXOPS) as *i64 96 d.nctr = sys_mmap(8*CD_MAXOPS) as *i64 97 d.nch = sys_mmap(CD_MAXOPS) 98 d.ntomb= sys_mmap(CD_MAXOPS) 99 d.cfirst=sys_mmap(8*CD_MAXOPS) as *i64 100 d.cnext =sys_mmap(8*CD_MAXOPS) as *i64 101 d.hsh = sys_mmap(8*CD_HASHN) as *i64 102 d.n = 1 // idx 0 = ROOT (rep 0, ctr 0) 103 d.err = 0 104 return d 105} 106 107func cd_hkey(rep: i64, ctr: i64) -> i64 { 108 var h: i64 = ctr*CD_MAGIC_1000003 + rep*CD_MAGIC_10007 + CD_MAGIC_12289 109 h = h ^ (h >> 15) 110 if h < 0 { h = 0 - h } 111 return h & (CD_HASHN - 1) 112} 113func cd_hfind(d: *CdDoc, rep: i64, ctr: i64) -> i64 { // node idx or -1 114 if rep==0 { if ctr==0 { return 0 } } 115 var h: i64 = cd_hkey(rep, ctr) 116 var g: i64 = 0 117 while g < CD_HASHN { 118 let v: i64 = d.hsh[h] 119 if v == 0 { return 0-1 } 120 let ix: i64 = v - 1 121 if d.nrep[ix]==rep { if d.nctr[ix]==ctr { return ix } } 122 h = (h + 1) & (CD_HASHN - 1) 123 g = g + 1 124 } 125 return 0-1 126} 127func cd_hput(d: *CdDoc, rep: i64, ctr: i64, ix: i64) -> i64 { 128 var h: i64 = cd_hkey(rep, ctr) 129 var g: i64 = 0 130 while g < CD_HASHN { 131 if d.hsh[h] == 0 { d.hsh[h] = ix + 1; return 0 } 132 h = (h + 1) & (CD_HASHN - 1) 133 g = g + 1 134 } 135 return 0-1 136} 137 138// id A(rep,ctr) newer-than B? RGA order: counter first, replica tie-break (bigger = first among siblings) 139func cd_newer(ra: i64, ca: i64, rb: i64, cb: i64) -> i64 { 140 if ca > cb { return 1 } 141 if ca < cb { return 0 } 142 if ra > rb { return 1 } 143 return 0 144} 145 146// link node ix under parent pidx at its RGA position (siblings sorted DESC by id) 147func cd_link(d: *CdDoc, pidx: i64, ix: i64) -> i64 { 148 let head: i64 = d.cfirst[pidx] 149 if head == 0 { d.cfirst[pidx] = ix; d.cnext[ix] = 0; return 0 } 150 if cd_newer(d.nrep[ix], d.nctr[ix], d.nrep[head], d.nctr[head]) == 1 { 151 d.cnext[ix] = head 152 d.cfirst[pidx] = ix 153 return 0 154 } 155 var cur: i64 = head 156 var run: i64 = 1 157 while run == 1 { 158 let nx: i64 = d.cnext[cur] 159 if nx == 0 { d.cnext[cur] = ix; d.cnext[ix] = 0; run = 0 } else { 160 if cd_newer(d.nrep[ix], d.nctr[ix], d.nrep[nx], d.nctr[nx]) == 1 { 161 d.cnext[ix] = nx 162 d.cnext[cur] = ix 163 run = 0 164 } else { cur = nx } 165 } 166 } 167 return 0 168} 169 170// unescape a 1-byte payload at s[i..e): returns the byte in out[0]; -1 malformed 171func cd_unesc(s: *u8, i: i64, e: i64, out: *u8) -> i64 { 172 if i >= e { return 0-1 } 173 let c: i64 = s[i] as i64 174 if c == 92 { // backslash escape: must be exactly two bytes 175 if i+2 != e { return 0-1 } 176 let c2: i64 = s[i+1] as i64 177 if c2==110 { out[0]=10 as u8; return 0 } 178 if c2==116 { out[0]=9 as u8; return 0 } 179 if c2==115 { out[0]=32 as u8; return 0 } 180 if c2==92 { out[0]=92 as u8; return 0 } 181 return 0-1 182 } 183 if i+1 != e { return 0-1 } // must be exactly one byte 184 out[0]=c as u8 185 return 0 186} 187 188// parse an id token "R" or "<rep>:<ctr>" at s[*ip..e) (stops at space/EOL); rc[0]=rep rc[1]=ctr; -1 malformed 189func cd_idtok(s: *u8, ip: *i64, e: i64, rc: *i64) -> i64 { 190 var i: i64 = ip[0] 191 if i < e { if s[i]==(82 as u8) { // "R" 192 var e2: i64 = i+1 193 var isend: i64 = 0 194 if e2 >= e { isend=1 } else { if s[e2]==(32 as u8) { isend=1 } } 195 if isend==1 { rc[0]=0; rc[1]=0; ip[0]=i+1; return 0 } 196 } } 197 let rep: i64 = cd_int(s, ip, e) 198 if rep < 0 { return 0-1 } 199 if ip[0] >= e { return 0-1 } 200 if s[ip[0]] != (58 as u8) { return 0-1 } 201 ip[0] = ip[0] + 1 202 let ctr: i64 = cd_int(s, ip, e) 203 if ctr < 0 { return 0-1 } 204 rc[0]=rep 205 rc[1]=ctr 206 return 0 207} 208 209// load an op log into the doc (two data passes packed into one: inserts create nodes with parent RESOLVED 210// on the spot when known, else deferred -- but merge always concatenates whole causally-ordered logs, so a 211// forward parent means the SIBLING log comes later; handle it with a deferred second pass). 212func cd_load(d: *CdDoc, log: *u8, ln: i64) -> i64 { 213 // deferred parents: entries (nodeidx, prep, pctr) 214 let dfi: *i64=sys_mmap(8*CD_MAXOPS) as *i64 215 let dfr: *i64=sys_mmap(8*CD_MAXOPS) as *i64 216 let dfc: *i64=sys_mmap(8*CD_MAXOPS) as *i64 217 var ndf: i64=0 218 // deferred deletes: (trep,tctr) 219 let ddr: *i64=sys_mmap(8*CD_MAXOPS) as *i64 220 let ddc: *i64=sys_mmap(8*CD_MAXOPS) as *i64 221 var ndd: i64=0 222 let rc: *i64=sys_mmap(32) as *i64 223 let bch: *u8=sys_mmap(8) 224 var i: i64=0 225 while i<ln { 226 var e: i64=i 227 while e<ln { if log[e]==(10 as u8) { break } e=e+1 } 228 var end: i64=e 229 if end>i { if log[end-1]==(13 as u8) { end=end-1 } } 230 if end>i+1 { 231 let kind: i64=log[i] as i64 232 if log[i+1] != (32 as u8) { d.err=1; return 0-1 } 233 let ip: *i64=sys_mmap(16) as *i64 234 ip[0]=i+2 235 if kind==73 { // I rep ctr afterid char 236 let rep: i64=cd_int(log, ip, end) 237 if rep<1 { d.err=1; return 0-1 } 238 if ip[0]>=end { d.err=1; return 0-1 } 239 ip[0]=ip[0]+1 240 let ctr: i64=cd_int(log, ip, end) 241 if ctr<1 { d.err=1; return 0-1 } 242 if ip[0]>=end { d.err=1; return 0-1 } 243 ip[0]=ip[0]+1 244 if cd_idtok(log, ip, end, rc)<0 { d.err=1; return 0-1 } 245 if ip[0]>=end { d.err=1; return 0-1 } 246 ip[0]=ip[0]+1 247 if cd_unesc(log, ip[0], end, bch)<0 { d.err=1; return 0-1 } 248 // dedup / conflict 249 let ex: i64=cd_hfind(d, rep, ctr) 250 if ex>=0 { 251 if d.nch[ex]==bch[0] { i=e+1 } else { d.err=3; return 0-1 } 252 } else { 253 if d.n>=CD_MAXOPS { d.err=4; return 0-1 } 254 let ix: i64=d.n 255 d.n=d.n+1 256 d.nrep[ix]=rep 257 d.nctr[ix]=ctr 258 d.nch[ix]=bch[0] 259 d.ntomb[ix]=0 as u8 260 d.cfirst[ix]=0 261 d.cnext[ix]=0 262 cd_hput(d, rep, ctr, ix) 263 let pix: i64=cd_hfind(d, rc[0], rc[1]) 264 if pix>=0 { cd_link(d, pix, ix) } else { 265 dfi[ndf]=ix 266 dfr[ndf]=rc[0] 267 dfc[ndf]=rc[1] 268 ndf=ndf+1 269 } 270 i=e+1 271 } 272 } else { if kind==68 { // D rep ctr targetid 273 let rep2: i64=cd_int(log, ip, end) 274 if rep2<1 { d.err=1; return 0-1 } 275 if ip[0]>=end { d.err=1; return 0-1 } 276 ip[0]=ip[0]+1 277 let ctr2: i64=cd_int(log, ip, end) 278 if ctr2<1 { d.err=1; return 0-1 } 279 if ip[0]>=end { d.err=1; return 0-1 } 280 ip[0]=ip[0]+1 281 if cd_idtok(log, ip, end, rc)<0 { d.err=1; return 0-1 } 282 ddr[ndd]=rc[0] 283 ddc[ndd]=rc[1] 284 ndd=ndd+1 285 i=e+1 286 } else { d.err=1; return 0-1 } } 287 } else { i=e+1 } 288 } 289 // resolve deferred parents (forward refs across concatenated logs) 290 var k: i64=0 291 while k<ndf { 292 let pix2: i64=cd_hfind(d, dfr[k], dfc[k]) 293 if pix2<0 { d.err=2; return 0-1 } 294 cd_link(d, pix2, dfi[k]) 295 k=k+1 296 } 297 // apply tombstones 298 k=0 299 while k<ndd { 300 let tix: i64=cd_hfind(d, ddr[k], ddc[k]) 301 if tix<1 { d.err=2; return 0-1 } // ROOT or missing = bad target 302 d.ntomb[tix]=1 as u8 303 k=k+1 304 } 305 return 0 306} 307 308// RGA linearization: preorder DFS (node byte, then children newest-first), tombstones skipped but traversed. 309// out gets the visible bytes; vis (optional, may be 0) gets the node idx per visible byte. Returns length. 310func cd_linear(d: *CdDoc, out: *u8, vis: *i64) -> i64 { 311 let stk: *i64=sys_mmap(8*CD_MAXOPS) as *i64 312 var sp: i64=0 313 var o: i64=0 314 // seed: ROOT's children pushed in REVERSE list order so the first child pops first 315 // (list is desc; we want to VISIT desc order; push onto stack reversed = collect then push backward) 316 let tmp: *i64=sys_mmap(8*CD_MAXOPS) as *i64 317 var tn: i64=0 318 var c: i64=d.cfirst[0] 319 while c != 0 { tmp[tn]=c; tn=tn+1; c=d.cnext[c] } 320 var q: i64=tn-1 321 while q>=0 { stk[sp]=tmp[q]; sp=sp+1; q=q-1 } 322 while sp>0 { 323 sp=sp-1 324 let ix: i64=stk[sp] 325 if (d.ntomb[ix] as i64)==0 { 326 out[o]=d.nch[ix] 327 if (vis as i64)!=0 { vis[o]=ix } 328 o=o+1 329 } 330 // push children reversed 331 tn=0 332 c=d.cfirst[ix] 333 while c != 0 { tmp[tn]=c; tn=tn+1; c=d.cnext[c] } 334 q=tn-1 335 while q>=0 { stk[sp]=tmp[q]; sp=sp+1; q=q-1 } 336 } 337 return o 338} 339 340func cd_loadfile(d: *CdDoc, path: *u8) -> i64 { 341 let buf: *u8=sys_mmap(CD_MAGIC_4194320) 342 let n: i64=cd_readfile(path, buf, CD_MAGIC_4194304) 343 if n<0 { return 0-9 } 344 return cd_load(d, buf, n) 345} 346 347// emit one escaped byte into dst 348func cd_escput(dst: *u8, off: i64, b: i64) -> i64 { 349 var o: i64=off 350 if b==10 { o=cd_cat(dst,o,"\\n" as *u8); return o } 351 if b==9 { o=cd_cat(dst,o,"\\t" as *u8); return o } 352 if b==32 { o=cd_cat(dst,o,"\\s" as *u8); return o } 353 if b==92 { o=cd_cat(dst,o,"\\\\" as *u8); return o } 354 dst[o]=b as u8 355 return o+1 356} 357 358// max counter used by replica `rep` in the log file (0 if none) 359func cd_maxctr(path: *u8, rep: i64) -> i64 { 360 let buf: *u8=sys_mmap(CD_MAGIC_4194320) 361 let n: i64=cd_readfile(path, buf, CD_MAGIC_4194304) 362 if n<0 { return 0 } 363 var best: i64=0 364 var i: i64=0 365 while i<n { 366 var e: i64=i 367 while e<n { if buf[e]==(10 as u8) { break } e=e+1 } 368 if e>i+2 { 369 let ip: *i64=sys_mmap(16) as *i64 370 ip[0]=i+2 371 let r: i64=cd_int(buf, ip, e) 372 if r==rep { if ip[0]<e { if buf[ip[0]]==(32 as u8) { 373 ip[0]=ip[0]+1 374 let c: i64=cd_int(buf, ip, e) 375 if c>best { best=c } 376 } } } 377 } 378 i=e+1 379 } 380 return best 381} 382 383func cd_fail(msg: *u8, code: i64) -> i64 { 384 cd_puts("COEDIT-FAIL " as *u8) 385 cd_puts(msg) 386 cd_puts(" err=" as *u8) 387 cd_num(code) 388 cd_puts(" (1=malformed 2=bad-ref 3=conflicting-duplicate-id 4=cap)\n" as *u8) 389 sys_exit(1) 390 return 1 391} 392 393// ---- R2 LIVE SYNC: multi-client convergence over HTTP. The canonical op log at <logpath> is the ONE union 394// point; because ops carry globally-unique ids and merge is idempotent + order-independent (proven by the 395// kernel gate), every client that pushes-then-pulls reaches the SAME text. This is what makes OF-C1 real 396// "real-time co-editing" rather than a lone merge engine. 397 398// dedup-append the op lines in body[0,blen) to the canonical log file: keep only ids not already present 399// (I-line "rep ctr", D-line "rep ctr" in a separate namespace). Returns #ops added (or <0 on I/O fail). 400func cd_seen_mark(seen: *i64, rep: i64, ctr: i64, isdel: i64) -> i64 { // returns 1 if NEW, 0 if already present 401 let key: i64=rep*CD_MAGIC_4294967296+ctr*2+isdel+1 402 var h: i64=cd_hkey(rep, ctr*2+isdel) 403 var g: i64=0 404 while g<CD_HASHN { 405 let v: i64=seen[h] 406 if v==0 { seen[h]=key; return 1 } 407 if v==key { return 0 } 408 h=(h+1)&(CD_HASHN-1) 409 g=g+1 410 } 411 return 0 412} 413// scan buf[0,blen) as op lines; for each unseen id, mark it and (if emit==1) copy the line into out. returns 414// bytes written to out. 415func cd_scan_dedup(buf: *u8, blen: i64, seen: *i64, out: *u8, emit: i64) -> i64 { 416 let ip: *i64=sys_mmap(16) as *i64 417 var oa: i64=0 418 var i: i64=0 419 while i<blen { 420 var e: i64=i 421 while e<blen { if buf[e]==(10 as u8) { break } e=e+1 } 422 var end: i64=e 423 if end>i { if buf[end-1]==(13 as u8) { end=end-1 } } 424 if end>i+2 { 425 var isdel: i64=0 426 if buf[i]==(68 as u8) { isdel=1 } 427 ip[0]=i+2 428 let rep: i64=cd_int(buf, ip, end) 429 var ctr: i64=0-1 430 if rep>0 { if ip[0]<end { if buf[ip[0]]==(32 as u8) { ip[0]=ip[0]+1; ctr=cd_int(buf, ip, end) } } } 431 if ctr>0 { 432 if cd_seen_mark(seen, rep, ctr, isdel)==1 { 433 if emit==1 { 434 var q: i64=i 435 while q<end { out[oa]=buf[q]; oa=oa+1; q=q+1 } 436 out[oa]=10 as u8; oa=oa+1 437 } 438 } 439 } 440 } 441 i=e+1 442 } 443 return oa 444} 445func cd_sync_append(logpath: *u8, body: *u8, blen: i64) -> i64 { 446 let cur: *u8=sys_mmap(CD_MAGIC_8388640) 447 var curn: i64=cd_readfile(logpath, cur, CD_MAGIC_8388608) 448 if curn<0 { curn=0 } 449 let seen: *i64=sys_mmap(8*CD_HASHN) as *i64 450 cd_scan_dedup(cur, curn, seen, 0 as *u8, 0) // pass 1: mark ids already in the log 451 let outapp: *u8=sys_mmap(CD_MAGIC_8388640) 452 let oa: i64=cd_scan_dedup(body, blen, seen, outapp, 1) // pass 2: emit only NEW body ops 453 if oa>0 { 454 let fd: i64=sys_openat_append(logpath, 0x1a4) 455 if fd<0 { return 0-1 } 456 sys_write(fd, outapp, oa) 457 sys_close(fd) 458 } 459 return oa 460} 461 462// THE live-sync request handler (the census OF-C1 needle). POST => dedup-append body ops to <logpath>; both 463// POST and GET => respond 200 with the FULL canonical merged log as the body (so the client materializes the 464// converged doc). Returns the response length written into out. 465func cd_live_sync(logpath: *u8, is_post: i64, body: *u8, blen: i64, out: *u8, outcap: i64) -> i64 { 466 if is_post==1 { cd_sync_append(logpath, body, blen) } 467 let logbuf: *u8=sys_mmap(CD_MAGIC_8388640) 468 var ln: i64=cd_readfile(logpath, logbuf, CD_MAGIC_8388608) 469 if ln<0 { ln=0 } 470 var o: i64=0 471 o=cd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: " as *u8) 472 o=cd_uint(out, o, ln) 473 o=cd_cat(out, o, "\r\nConnection: close\r\n\r\n" as *u8) 474 var i: i64=0 475 while i<ln { out[o]=logbuf[i]; o=o+1; i=i+1 } 476 return o 477} 478 479// header terminator offset (index just past \r\n\r\n), or -1 if headers not yet complete 480func cd_hdrend(req: *u8, rn: i64) -> i64 { 481 var i: i64=0 482 while i+3<rn { if req[i]==(13 as u8) { if req[i+1]==(10 as u8) { if req[i+2]==(13 as u8) { if req[i+3]==(10 as u8) { return i+4 } } } } i=i+1 } 483 return 0-1 484} 485// parse the "Content-Length:" header value, searching only the header region; -1 if absent 486func cd_contentlen(req: *u8, he: i64) -> i64 { 487 let pat: *u8="Content-Length:" as *u8 488 let pl: i64=cd_slen(pat) 489 var i: i64=0 490 while i+pl<=he { 491 var m: i64=1 492 var j: i64=0 493 while j<pl { if req[i+j]!=pat[j] { m=0; j=pl } else { j=j+1 } } 494 if m==1 { 495 var k: i64=i+pl 496 while k<he { if req[k]==(32 as u8) { k=k+1 } else { break } } 497 var v: i64=0 498 var nd: i64=0 499 while k<he { let c: i64=req[k] as i64; if c>=48 { if c<=57 { v=v*10+(c-48); nd=nd+1; k=k+1 } else { break } } else { break } } 500 if nd>0 { return v } 501 return 0 502 } 503 i=i+1 504 } 505 return 0-1 506} 507// minimal request parse: is it POST? and where is the body (after \r\n\r\n)? sets bp[0]=body offset, bp[1]=len 508func cd_parsereq(req: *u8, rn: i64, bp: *i64) -> i64 { 509 var is_post: i64=0 510 if rn>=4 { if req[0]==(80 as u8) { if req[1]==(79 as u8) { if req[2]==(83 as u8) { if req[3]==(84 as u8) { is_post=1 } } } } } 511 let he: i64=cd_hdrend(req, rn) 512 if he<0 { bp[0]=rn; bp[1]=0 } else { bp[0]=he; bp[1]=rn-he } 513 return is_post 514} 515 516func cd_addr(out: *u8, port: i64) -> i64 { 517 out[0]=2 as u8; out[1]=0 as u8 518 out[2]=((port>>8)&0xff) as u8; out[3]=(port&0xff) as u8 519 out[4]=127 as u8; out[5]=0 as u8; out[6]=0 as u8; out[7]=1 as u8 520 var i: i64=8; while i<16 { out[i]=0 as u8; i=i+1 } return 0 521} 522 523// serve <logpath> <port> [maxconns]: accept-loop live-sync daemon. maxconns=0 => serve forever. 524func cd_serve(logpath: *u8, port: i64, maxconns: i64) -> i64 { 525 let addr: *u8=sys_mmap(16) 526 cd_addr(addr, port) 527 let lfd: i64=sys_socket(AF_INET, SOCK_STREAM, 0) 528 if lfd<0 { return 0-1 } 529 let one: *i64=sys_mmap(8) as *i64; one[0]=1 // SO_REUSEADDR: rebind through a prior server's TIME_WAIT 530 sys_setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, one as *u8, 4) 531 if sys_bind(lfd, addr, 16)<0 { return 0-2 } 532 if sys_listen(lfd, 16)<0 { return 0-3 } 533 let reqb: *u8=sys_mmap(CD_MAGIC_8388640) 534 let resb: *u8=sys_mmap(CD_MAGIC_8388640) 535 let bp: *i64=sys_mmap(16) as *i64 536 var served: i64=0 537 var run: i64=1 538 while run==1 { 539 let cfd: i64=sys_accept(lfd) 540 if cfd<0 { run=0 } else { 541 // read the request: loop until headers complete AND Content-Length body bytes are in (or EOF). 542 // For CL:0 (a pure pull) the request is complete at end-of-headers -- no blocking wait for a body. 543 var rn: i64=0 544 var g: i64=1 545 while g==1 { 546 let he: i64=cd_hdrend(reqb, rn) 547 if he>=0 { 548 var cl: i64=cd_contentlen(reqb, he) 549 if cl<0 { cl=0 } 550 if rn>=he+cl { g=0 } 551 } 552 if g==1 { 553 if rn>=CD_MAGIC_8388600 { g=0 } else { 554 let r: i64=sys_read(cfd, (reqb as i64+rn) as *u8, CD_MAGIC_8388600-rn) 555 if r<=0 { g=0 } else { rn=rn+r } 556 } 557 } 558 } 559 let is_post: i64=cd_parsereq(reqb, rn, bp) 560 // clean shutdown: a GET/POST to /shutdown stops the accept loop (so a caller reaps us -- no kill race) 561 var isdown: i64=0 562 if rn>=13 { var s: i64=0; while s<rn-13 { if reqb[s]==(47 as u8) { if reqb[s+1]==(115 as u8) { if reqb[s+2]==(104 as u8) { if reqb[s+3]==(117 as u8) { if reqb[s+4]==(116 as u8) { isdown=1; s=rn } } } } } s=s+1 } } 563 if isdown==1 { 564 let ok: *u8="HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nbye" as *u8 565 rv_write_all_fd(cfd, ok, cd_slen(ok)) 566 sys_close(cfd) 567 run=0 568 } else { 569 let on: i64=cd_live_sync(logpath, is_post, (reqb as i64+bp[0]) as *u8, bp[1], resb, CD_MAGIC_8388608) 570 rv_write_all_fd(cfd, resb, on) 571 sys_close(cfd) 572 served=served+1 573 if maxconns>0 { if served>=maxconns { run=0 } } 574 } 575 } 576 } 577 sys_close(lfd) 578 return served 579} 580 581func rv_write_all_fd(fd: i64, buf: *u8, n: i64) -> i64 { 582 var off: i64=0 583 while off<n { 584 let w: i64=sys_write(fd, (buf as i64+off) as *u8, n-off) 585 if w<=0 { return 0-1 } 586 off=off+w 587 } 588 return 0 589} 590 591func main(argc: i64, argv: *i64) -> i64 { 592 if argc<3 { cd_puts("usage: nx_coedit apply <log> <out.txt> | ins <log> <rep> <ctr> <afterid> <char> | del <log> <rep> <ctr> <targetid> | merge <A> <B> <out> | edit <log> <rep> <pos> ins <text> | edit <log> <rep> <pos> del <count> | serve <log> <port> [maxconns]\n" as *u8); sys_exit(2); return 2 } 593 let cmd: *u8=argv[1] as *u8 594 595 if cd_streq(cmd,"apply" as *u8)==1 { 596 if argc<4 { cd_puts("usage: nx_coedit apply <log> <out.txt>\n" as *u8); sys_exit(2); return 2 } 597 let d: *CdDoc=cd_new() 598 if cd_loadfile(d, argv[2] as *u8)<0 { return cd_fail("load" as *u8, d.err) } 599 let out: *u8=sys_mmap(CD_MAXOPS+16) 600 let n: i64=cd_linear(d, out, 0 as *i64) 601 if cd_writefile(argv[3] as *u8, out, n)<0 { return cd_fail("write" as *u8, 0) } 602 cd_puts("COEDIT-APPLY-OK bytes=" as *u8); cd_num(n); cd_puts(" ops=" as *u8); cd_num(d.n-1); cd_puts("\n" as *u8) 603 sys_exit(0); return 0 604 } 605 if cd_streq(cmd,"ins" as *u8)==1 { 606 if argc<7 { cd_puts("usage: nx_coedit ins <log> <rep> <ctr> <afterid> <char>\n" as *u8); sys_exit(2); return 2 } 607 let line: *u8=sys_mmap(256) 608 var o: i64=cd_cat(line,0,"I " as *u8) 609 o=cd_cat(line,o,argv[3] as *u8); line[o]=32 as u8; o=o+1 610 o=cd_cat(line,o,argv[4] as *u8); line[o]=32 as u8; o=o+1 611 o=cd_cat(line,o,argv[5] as *u8); line[o]=32 as u8; o=o+1 612 o=cd_cat(line,o,argv[6] as *u8); line[o]=10 as u8; o=o+1 613 if cd_appendfile(argv[2] as *u8, line, o)<0 { return cd_fail("append" as *u8, 0) } 614 cd_puts("COEDIT-INS-OK\n" as *u8) 615 sys_exit(0); return 0 616 } 617 if cd_streq(cmd,"del" as *u8)==1 { 618 if argc<6 { cd_puts("usage: nx_coedit del <log> <rep> <ctr> <targetid>\n" as *u8); sys_exit(2); return 2 } 619 let line: *u8=sys_mmap(256) 620 var o: i64=cd_cat(line,0,"D " as *u8) 621 o=cd_cat(line,o,argv[3] as *u8); line[o]=32 as u8; o=o+1 622 o=cd_cat(line,o,argv[4] as *u8); line[o]=32 as u8; o=o+1 623 o=cd_cat(line,o,argv[5] as *u8); line[o]=10 as u8; o=o+1 624 if cd_appendfile(argv[2] as *u8, line, o)<0 { return cd_fail("append" as *u8, 0) } 625 cd_puts("COEDIT-DEL-OK\n" as *u8) 626 sys_exit(0); return 0 627 } 628 if cd_streq(cmd,"merge" as *u8)==1 { 629 if argc<5 { cd_puts("usage: nx_coedit merge <A> <B> <out>\n" as *u8); sys_exit(2); return 2 } 630 // validate the union parses cleanly (incl conflict detection), then write A-lines + B-lines deduped by id 631 let a: *u8=sys_mmap(CD_MAGIC_4194320) 632 let an: i64=cd_readfile(argv[2] as *u8, a, CD_MAGIC_4194304) 633 if an<0 { return cd_fail("read-A" as *u8, 0) } 634 let b: *u8=sys_mmap(CD_MAGIC_4194320) 635 let bn: i64=cd_readfile(argv[3] as *u8, b, CD_MAGIC_4194304) 636 if bn<0 { return cd_fail("read-B" as *u8, 0) } 637 let both: *u8=sys_mmap(CD_MAGIC_8388640) 638 var bo: i64=0 639 var i: i64=0 640 while i<an { both[bo]=a[i]; bo=bo+1; i=i+1 } 641 if bo>0 { if both[bo-1]!=(10 as u8) { both[bo]=10 as u8; bo=bo+1 } } 642 i=0 643 while i<bn { both[bo]=b[i]; bo=bo+1; i=i+1 } 644 let d: *CdDoc=cd_new() 645 if cd_load(d, both, bo)<0 { return cd_fail("merge-validate" as *u8, d.err) } 646 // dedup pass: keep first occurrence of each op id (I ids from nodes; D lines dedup by (rep,ctr) too) 647 let seen: *i64=sys_mmap(8*CD_HASHN) as *i64 648 let out: *u8=sys_mmap(CD_MAGIC_8388640) 649 var oo: i64=0 650 i=0 651 while i<bo { 652 var e: i64=i 653 while e<bo { if both[e]==(10 as u8) { break } e=e+1 } 654 if e>i+2 { 655 let ip: *i64=sys_mmap(16) as *i64 656 ip[0]=i+2 657 let rep: i64=cd_int(both, ip, e) 658 var ctr: i64=0-1 659 if rep>0 { if ip[0]<e { if both[ip[0]]==(32 as u8) { ip[0]=ip[0]+1; ctr=cd_int(both, ip, e) } } } 660 var dup: i64=0 661 if ctr>0 { 662 var isdel: i64=0 663 if both[i]==(68 as u8) { isdel=1 } 664 let key: i64=rep*CD_MAGIC_4294967296+ctr*2+isdel+1 665 var h: i64=cd_hkey(rep, ctr*2+isdel) 666 var g: i64=0 667 while g<CD_HASHN { 668 let v: i64=seen[h] 669 if v==0 { seen[h]=key; g=CD_HASHN } else { 670 if v==key { dup=1; g=CD_HASHN } else { h=(h+1)&(CD_HASHN-1); g=g+1 } 671 } 672 } 673 } 674 if dup==0 { 675 var q: i64=i 676 while q<e { out[oo]=both[q]; oo=oo+1; q=q+1 } 677 out[oo]=10 as u8 678 oo=oo+1 679 } 680 } 681 i=e+1 682 } 683 if cd_writefile(argv[4] as *u8, out, oo)<0 { return cd_fail("write" as *u8, 0) } 684 cd_puts("COEDIT-MERGE-OK ops=" as *u8); cd_num(d.n-1); cd_puts("\n" as *u8) 685 sys_exit(0); return 0 686 } 687 if cd_streq(cmd,"edit" as *u8)==1 { 688 if argc<6 { cd_puts("usage: nx_coedit edit <log> <rep> <pos> ins <text> | edit <log> <rep> <pos> del <count>\n" as *u8); sys_exit(2); return 2 } 689 let ip0: *i64=sys_mmap(16) as *i64 690 ip0[0]=0 691 let reps: *u8=argv[3] as *u8 692 let rep: i64=cd_int(reps, ip0, cd_slen(reps)) 693 ip0[0]=0 694 let poss: *u8=argv[4] as *u8 695 let pos: i64=cd_int(poss, ip0, cd_slen(poss)) 696 if rep<1 { return cd_fail("bad-replica" as *u8, 1) } 697 if pos<0 { return cd_fail("bad-pos" as *u8, 1) } 698 let mode: *u8=argv[5] as *u8 699 let d: *CdDoc=cd_new() 700 if cd_loadfile(d, argv[2] as *u8)<0 { return cd_fail("load" as *u8, d.err) } 701 let cur: *u8=sys_mmap(CD_MAXOPS+16) 702 let vis: *i64=sys_mmap(8*(CD_MAXOPS+16)) as *i64 703 let vn: i64=cd_linear(d, cur, vis) 704 var ctr: i64=cd_maxctr(argv[2] as *u8, rep) 705 if cd_streq(mode,"ins" as *u8)==1 { 706 if argc<7 { cd_puts("usage: nx_coedit edit <log> <rep> <pos> ins <text>\n" as *u8); sys_exit(2); return 2 } 707 if pos>vn { return cd_fail("pos-out-of-range" as *u8, 2) } 708 let txt: *u8=argv[6] as *u8 709 let tl: i64=cd_slen(txt) 710 if tl<1 { return cd_fail("empty-text" as *u8, 1) } 711 // anchor: R (pos 0) or the id of the visible byte at pos-1; then chain each new byte after the last 712 var isroot: i64=1 713 var arep: i64=0 714 var actr: i64=0 715 if pos>0 { isroot=0; let aix: i64=vis[pos-1]; arep=d.nrep[aix]; actr=d.nctr[aix] } 716 let line: *u8=sys_mmap(256) 717 var t: i64=0 718 while t<tl { 719 ctr=ctr+1 720 var o: i64=cd_cat(line,0,"I " as *u8) 721 o=cd_uint(line,o,rep); line[o]=32 as u8; o=o+1 722 o=cd_uint(line,o,ctr); line[o]=32 as u8; o=o+1 723 if isroot==1 { o=cd_cat(line,o,"R" as *u8) } else { o=cd_uint(line,o,arep); line[o]=58 as u8; o=o+1; o=cd_uint(line,o,actr) } 724 line[o]=32 as u8; o=o+1 725 o=cd_escput(line,o,txt[t] as i64) 726 line[o]=10 as u8; o=o+1 727 if cd_appendfile(argv[2] as *u8, line, o)<0 { return cd_fail("append" as *u8, 0) } 728 isroot=0 729 arep=rep 730 actr=ctr 731 t=t+1 732 } 733 cd_puts("COEDIT-EDIT-OK ins ops=" as *u8); cd_num(tl); cd_puts("\n" as *u8) 734 sys_exit(0); return 0 735 } 736 if cd_streq(mode,"del" as *u8)==1 { 737 if argc<7 { cd_puts("usage: nx_coedit edit <log> <rep> <pos> del <count>\n" as *u8); sys_exit(2); return 2 } 738 ip0[0]=0 739 let cnts: *u8=argv[6] as *u8 740 let cnt: i64=cd_int(cnts, ip0, cd_slen(cnts)) 741 if cnt<1 { return cd_fail("bad-count" as *u8, 1) } 742 if pos+cnt>vn { return cd_fail("range-out-of-range" as *u8, 2) } 743 let line: *u8=sys_mmap(256) 744 var t: i64=0 745 while t<cnt { 746 ctr=ctr+1 747 let tix: i64=vis[pos+t] 748 var o: i64=cd_cat(line,0,"D " as *u8) 749 o=cd_uint(line,o,rep); line[o]=32 as u8; o=o+1 750 o=cd_uint(line,o,ctr); line[o]=32 as u8; o=o+1 751 o=cd_uint(line,o,d.nrep[tix]); line[o]=58 as u8; o=o+1 752 o=cd_uint(line,o,d.nctr[tix]) 753 line[o]=10 as u8; o=o+1 754 if cd_appendfile(argv[2] as *u8, line, o)<0 { return cd_fail("append" as *u8, 0) } 755 t=t+1 756 } 757 cd_puts("COEDIT-EDIT-OK del ops=" as *u8); cd_num(cnt); cd_puts("\n" as *u8) 758 sys_exit(0); return 0 759 } 760 cd_puts("nx_coedit edit: mode must be ins|del\n" as *u8) 761 sys_exit(2); return 2 762 } 763 if cd_streq(cmd,"serve" as *u8)==1 { 764 if argc<4 { cd_puts("usage: nx_coedit serve <log> <port> [maxconns]\n" as *u8); sys_exit(2); return 2 } 765 let ip0: *i64=sys_mmap(16) as *i64 766 ip0[0]=0 767 let ports: *u8=argv[3] as *u8 768 let port: i64=cd_int(ports, ip0, cd_slen(ports)) 769 if port<1 { return cd_fail("bad-port" as *u8, 1) } 770 var maxc: i64=0 771 if argc>4 { ip0[0]=0; let ms: *u8=argv[4] as *u8; maxc=cd_int(ms, ip0, cd_slen(ms)) } 772 cd_puts("COEDIT-SERVE listening port=" as *u8); cd_num(port); cd_puts(" log=" as *u8); cd_puts(argv[2] as *u8); cd_puts("\n" as *u8) 773 let served: i64=cd_serve(argv[2] as *u8, port, maxc) 774 if served<0 { return cd_fail("serve" as *u8, 0-served) } 775 cd_puts("COEDIT-SERVE-DONE served=" as *u8); cd_num(served); cd_puts("\n" as *u8) 776 sys_exit(0); return 0 777 } 778 cd_puts("nx_coedit: unknown command (apply|ins|del|merge|edit|serve)\n" as *u8) 779 sys_exit(2); return 2 780}