@pointyink/rogue 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import a from"./rogue.wasm.js";let s;function u(){return s??=Uint8Array.from(atob(a),h=>h.charCodeAt(0)),s}class o{static async create({wasmBytes:t,replicaId:e=1,capacity:n=1<<20}={}){t??=u();let r;const{instance:_}=await WebAssembly.instantiate(t,{env:{jsLog:()=>{}}}),i=_.exports;return r=i.memory,i.rogue_init(e,n),new o(i,r)}constructor(t,e){this._exp=t,this._mem=e,this._inPtr=t.rogue_in_ptr(),this._outPtr=t.rogue_out_ptr(),this._length=0}get length(){return this._length}insert(t,e){const n=new Uint16Array(this._mem.buffer,this._inPtr,e.length);for(let r=0;r<e.length;r++)n[r]=e.charCodeAt(r);this._exp.rogue_insert(t,0,e.length*2),this._length+=e.length}delete(t,e){this._exp.rogue_hide(t,e),this._length-=e}toString(){const t=this._exp.rogue_render();return new TextDecoder("utf-16le").decode(new Uint8Array(this._mem.buffer,this._outPtr,t*2))}}export{o as RogueText};
1
+ import a from"./rogue.wasm.js";let o;function u(){return o??=Uint8Array.from(atob(a),h=>h.charCodeAt(0)),o}class s{static async create({wasmBytes:t,replicaId:e=1,capacity:n=1<<20}={}){t??=u();let r;const{instance:_}=await WebAssembly.instantiate(t,{env:{jsLog:()=>{}}}),i=_.exports;if(r=i.memory,!i.rogue_init(e,n))throw new Error("rogue_init failed \u2014 out of memory allocating the CRDT");return new s(i,r)}constructor(t,e){this._exp=t,this._mem=e,this._inPtr=t.rogue_in_ptr(),this._outPtr=t.rogue_out_ptr(),this._length=0}get length(){return this._length}insert(t,e){const n=new Uint16Array(this._mem.buffer,this._inPtr,e.length);for(let r=0;r<e.length;r++)n[r]=e.charCodeAt(r);this._exp.rogue_insert(t,0,e.length*2),this._length+=e.length}delete(t,e){this._exp.rogue_hide(t,e),this._length-=e}toString(){this._exp.rogue_render();const t=this._exp.rogue_out_len();return new TextDecoder("utf-16le").decode(new Uint8Array(this._mem.buffer,this._outPtr,t))}}export{s as RogueText};
@@ -0,0 +1 @@
1
+ import{anchorForRange as T,endOfDocPos as P}from"./comments.js";function E(o,s,l,d){let i=o.visIxOf(l);if(i===null){const a=o.nextVisId(l);i=a===null?null:o.visIxOf(a)}let r=o.visIxOf(d);if(r===null){const a=o.prevVisId(d);r=a===null?null:o.visIxOf(a)}if(i===null||r===null||i>r)return null;const u=s.visIxToDomPos(i);if(!u)return null;const f=s.visIxToDomPos(r+1)??P(s,o.visLen());return f?{startNode:u.node,startOffset:u.offset,endNode:f.node,endOffset:f.offset}:null}function I({crdt:o,rope:s,baseUrl:l,fetchFn:d,rerender:i,onError:r,markUserEdit:u,getSelectionRange:f}){const a=d??((e,t)=>globalThis.fetch(e,t));let m=[],O=!1;async function g(e,t){const n=await a(l+e,{credentials:"same-origin",headers:{"Content-Type":"application/json"},...t});if(!n.ok)throw new Error(`suggestions request failed: ${n.status} ${e}`);return n.status===204?null:n.json()}function N(e){return{...e,range:E(o,s,e.start_id,e.end_id)}}function w(){return m.map(N)}function y(){O||i?.(w())}async function p(){m=await g("")??[];const e=w();return y(),e}async function v(e){try{await e(),await p()}catch(t){throw r?.(t),t}}return{load:p,suggestions:()=>w(),create:e=>{const t=f?.();if(!t)return Promise.reject(new Error("no text selected"));const n=T(o,s,t);return n?v(()=>g("",{method:"POST",body:JSON.stringify({...n,replacement:e})})):Promise.reject(new Error("empty selection cannot be anchored"))},accept:async e=>{const t=m.find(c=>c.id===e);if(!t)throw new Error("suggestion not found");const n=E(o,s,t.start_id,t.end_id);if(!n){const c=new Error("suggestion span is stale \u2014 review the current text before accepting");throw r?.(c),c}u?.();const x=n.startNode.ownerDocument;if(!x){const c=new Error("suggestion span has no owning document");throw r?.(c),c}const h=x.createRange();h.setStart(n.startNode,n.startOffset),h.setEnd(n.endNode,n.endOffset),h.deleteContents(),t.replacement.length>0&&h.insertNode(x.createTextNode(t.replacement)),await v(()=>g(`/${e}/accept`,{method:"POST"}))},reject:e=>v(()=>g(`/${e}`,{method:"DELETE"})),handleDocumentChange:e=>{y()},handleRemoteChange:()=>p(),destroy:()=>{O=!0}}}export{I as createSuggestions,E as resolveSuggestionAnchor};
@@ -1 +1 @@
1
- import{enqueue as v,ackMany as C,pendingFor as x}from"./oplog.js";import"@pointyink/serde/multi_op_v0";import*as I from"@pointyink/serde/ack_v0";import*as S from"@pointyink/serde/node_id_v0";const U=3,P=4,R=5,o=6;function q(i){if(i.byteLength<S.SIZE)return null;const n=new DataView(i.buffer,i.byteOffset,i.byteLength),r=S.decode(n,0);return`${r.replica_id}:${r.sequence}`}function F(i,n){const r=new Uint8Array(o+n.byteLength),u=new DataView(r.buffer);return u.setUint16(0,i,!0),u.setUint32(2,n.byteLength,!0),r.set(n,o),r}const M="rogue.jwt";function N({wsUrl:i,docId:n,mergeOp:r,getToken:u,onStatus:k,onReady:D}){let c=null,f=1e3,d=!1,y=!1,h=null,p=Promise.resolve();function m(e){k?.(e)}function L(e){c?.send(F(U,e))}async function O(){try{const e=await x(n);for(const t of e){try{r(t.payload)}catch(w){console.error("[sync-client] local replay apply failed:",w)}L(t.payload)}}catch(e){console.error("[sync-client] replay failed:",e)}}function A(e){if(e.byteLength<2)return;const t=new DataView(e),w=t.getUint16(0,!0);switch(w){case U:{if(e.byteLength<o)return;const s=t.getUint32(2,!0);if(e.byteLength<o+s)return;const a=new Uint8Array(e,o,s);r(a);return}case P:{if(e.byteLength<o)return;const s=t.getUint32(2,!0),a=new Uint8Array(e,o,s);try{const E=I.decode(a).group_ids.map(g=>`${g.replica_id}:${g.sequence}`);E.length>0&&C(n,E).catch(g=>console.error("[sync-client] ack drop failed:",g))}catch(l){console.error("[sync-client] bad ack frame:",l)}return}case R:{if(e.byteLength<o)return;const s=t.getUint32(2,!0),a=new Uint8Array(e,o,s);let l;try{l=JSON.parse(new TextDecoder().decode(a))}catch{return}l?.type==="sync_done"&&(y||O().then(()=>{y=!0,D?.()}));return}default:console.warn("[sync-client] unknown disc on incoming frame:",w)}}function _(){d||(h=setTimeout(b,f),f=Math.min(f*2,3e4))}async function b(){if(d)return;m("connecting"),y=!1;let e;try{e=await u()}catch(t){console.error("[sync-client] token fetch failed:",t),m("disconnected"),_();return}d||(c=new WebSocket(i,[M,e]),c.binaryType="arraybuffer",c.addEventListener("open",()=>{f=1e3,m("connected")}),c.addEventListener("close",()=>{m("disconnected"),_()}),c.addEventListener("message",t=>{t.data instanceof ArrayBuffer?A(t.data):console.warn("[sync-client] unexpected text frame:",t.data)}))}function T(e){p=p.then(async()=>{const t=q(e);t&&await v(n,t,e),L(e)}).catch(t=>console.error("[sync-client] enqueue/send failed:",t))}function V(){if(d=!0,h&&(clearTimeout(h),h=null),c){try{c.close()}catch{}c=null}}return b(),{send:T,destroy:V,isReady:()=>y}}export{N as createSyncClient};
1
+ import{enqueue as Y,ackMany as Z,pendingFor as z,clearDoc as Q}from"./oplog.js";import{cacheOp as V,cachedOps as X,clearDoc as I}from"./doc-cache.js";import{advanceWatermark as ee,mergeAndAdvance as te,appendSince as ne}from"./checkpoint.js";import"@pointyink/serde/multi_op_v0";import*as ce from"@pointyink/serde/ack_v0";import*as x from"@pointyink/serde/node_id_v0";const B=3,re=4,P=5,i=6;function oe(a){if(a.byteLength<x.SIZE)return null;const r=new DataView(a.buffer,a.byteOffset,a.byteLength),s=x.decode(r,0);return`${s.replica_id}:${s.sequence}`}function M(a,r){const s=new Uint8Array(i+r.byteLength),m=new DataView(s.buffer);return m.setUint16(0,a,!0),m.setUint32(2,r.byteLength,!0),s.set(r,i),s}const ie="rogue.jwt";function ue({wsUrl:a,docId:r,mergeOp:s,getToken:m,onStatus:R,onReady:q,onSyncStart:L,onProgress:D,onCommentChanged:F,onSuggestionChanged:$,onDocDeleted:N}){let o=null,p=1e3,u=!1,g=!1,y=0,w=0,O=0;function k(e){if(!D||y===0)return;const t=Date.now();!e&&t-O<100||(O=t,D(Math.min(1,w/y)))}let l=null,A=Promise.resolve();const b={};function E(e,t){te(s,b,e),w+=e.byteLength,k(!1),t&&V(r,e).catch(c=>console.error("[sync-client] cache write failed:",c))}function T(e){E(e,!0)}function d(e){R?.(e)}function U(e){o?.send(M(B,e))}async function W(){try{const e=await z(r);for(const t of e){try{T(t.payload)}catch(c){console.error("[sync-client] local replay apply failed:",c)}U(t.payload)}}catch(e){console.error("[sync-client] replay failed:",e)}}function J(e){if(e.byteLength<2)return;const t=new DataView(e),c=t.getUint16(0,!0);switch(c){case B:{if(e.byteLength<i)return;const f=t.getUint32(2,!0);if(e.byteLength<i+f)return;const h=new Uint8Array(e,i,f);try{T(h)}catch(n){console.error("[sync-client] merge failed:",n)}return}case re:{if(e.byteLength<i)return;const f=t.getUint32(2,!0),h=new Uint8Array(e,i,f);try{const C=ce.decode(h).group_ids.map(_=>`${_.replica_id}:${_.sequence}`);C.length>0&&Z(r,C).catch(_=>console.error("[sync-client] ack drop failed:",_))}catch(n){console.error("[sync-client] bad ack frame:",n)}return}case P:{if(e.byteLength<i)return;const f=t.getUint32(2,!0),h=new Uint8Array(e,i,f);let n;try{n=JSON.parse(new TextDecoder().decode(h))}catch{return}if(n?.type==="sync_start"){y=typeof n.bytes=="number"?n.bytes:0,w=0,L?.(y);return}if(n?.type==="sync_done"&&(g||W().then(()=>{g=!0,k(!0),q?.()})),n?.type==="comment_changed"&&F?.({comment_id:n.comment_id,op:n.op}),n?.type==="suggestion_changed"&&$?.({suggestion_id:n.suggestion_id,op:n.op}),n?.type==="doc_deleted"){if(u=!0,l&&(clearTimeout(l),l=null),o){try{o.close()}catch{}o=null}Q(r).catch(()=>{}),I(r).catch(()=>{}),N?.(r)}return}default:console.warn("[sync-client] unknown disc on incoming frame:",c)}}function S(){u||(l=setTimeout(v,p),p=Math.min(p*2,3e4))}async function v(){if(u)return;d("connecting"),g=!1;let e;try{e=await m()}catch(c){console.error("[sync-client] token fetch failed:",c),d("disconnected"),S();return}if(u)return;const t=ne(a,b);o=new WebSocket(t,[ie,e]),o.binaryType="arraybuffer",o.addEventListener("open",()=>{p=1e3,d("connected")}),o.addEventListener("close",()=>{d("disconnected"),S()}),o.addEventListener("message",c=>{c.data instanceof ArrayBuffer?J(c.data):console.warn("[sync-client] unexpected text frame:",c.data)})}function j(e){ee(b,e),V(r,e).catch(t=>console.error("[sync-client] cache write failed:",t)),A=A.then(async()=>{const t=oe(e);t&&await Y(r,t,e),U(e)}).catch(t=>console.error("[sync-client] enqueue/send failed:",t))}function G(){const e=new TextEncoder().encode('{"type":"delete_doc"}');o?.send(M(P,e))}function H(){if(u=!0,l&&(clearTimeout(l),l=null),o){try{o.close()}catch{}o=null}}async function K(){d("connecting");try{const e=await X(r);if(e.length>0){let t=0;for(const c of e)t+=c.byteLength;y=t,w=0,L?.(t)}for(const t of e)try{E(t,!1)}catch(c){console.error("[sync-client] cache replay failed:",c)}}catch(e){console.error("[sync-client] cache load failed:",e)}v()}return K(),{send:j,sendDelete:G,destroy:H,isReady:()=>g}}export{ue as createSyncClient};
package/dist/sync.js CHANGED
@@ -1,4 +1,7 @@
1
- import{diff_match_patch as yn}from"diff-match-patch";import vn from"dompurify";import bn from"./rogue.wasm.js";import{buildRope as Re,defaultSerializeNode as _n,defaultOmitNode as Sn,defaultUnwrapNode as wn,escapeText as Ln}from"./dom_rope.js";const Nn=new yn;function be(l,_=document){const I=_.createElement("template");return I.innerHTML=l,I.innerHTML===l}function b(l,_){if(!globalThis.__rogueProfile)return _();performance.mark(`${l}-start`);const I=_();return performance.mark(`${l}-end`),performance.measure(l,`${l}-start`,`${l}-end`),I}function X(l,_){if(!_.isConnected)return{};const I=l.getRange(_);if(I)return{minIx:I[0],maxIx:I[1]};let y=_.previousSibling,v=null;for(;y&&!(v=l.getRange(y));)y=y.previousSibling;let p=_.nextSibling,$=null;for(;p&&!($=l.getRange(p));)p=p.nextSibling;return{...v?{minIx:v[0]}:{startEdge:!0},...$?{maxIx:$[1]}:{endEdge:!0}}}async function Tn({element:l,replicaId:_=Math.floor(Math.random()*65534)+1,onCommit:I,paste:y,doc:v=document,win:p=window,serializeNode:$=_n,omitNode:Z=Sn,unwrapNode:ee=wn}){const Be=Uint8Array.from(atob(bn),e=>e.charCodeAt(0));let L;const{instance:Fe}=await WebAssembly.instantiate(Be,{env:{jsLog:(e,n)=>console.log("[wasm]",new TextDecoder().decode(new Uint8Array(L.buffer,e,n)))}}),_e=Fe.exports;L=_e.memory;const{rogue_init:He,rogue_in_ptr:Ue,rogue_out_ptr:je,rogue_out_len:ne,rogue_render:F,rogue_render_range:Ke,rogue_begin_batch:qe,rogue_hide:Je,rogue_insert:ze,rogue_commit_batch:Ge,rogue_undo:We,rogue_redo:Ye,rogue_merge:Qe,rogue_revert_tail:Xe,rogue_op_is_structural:Ze,rogue_deinit:en,rogue_op_bounds:nn,rogue_vis_len:H,rogue_id_at:T,rogue_vis_ix:D,rogue_prev_vis:te,rogue_next_vis:re,rogue_head:tn,rogue_ca_len:rn,rogue_set_debug_logging:Se}=_e;He(_,1<<20);const U=Ue(),ie=je();let d=Re(l,$,Z,ee);function M(){d.update(null,null,l)}let C=!1;try{C=/(?:^|[?&])debug=sync(?:&|$)/.test(globalThis.location?.search??"")}catch{}Se(C?1:0);const we=()=>!!globalThis.__rogueForceFullSync;let j="";function m(e){const n=new TextEncoder().encode(e);return new Uint8Array(L.buffer,U,n.length).set(n),n.length}function oe(e){return new Uint8Array(L.buffer,U,e.byteLength).set(e),e.byteLength}function se(){const e=ne(),n=new Uint8Array(e);return n.set(new Uint8Array(L.buffer,ie,e)),n}function on(e,n){const t=new Uint16Array(L.buffer,U+e,n.length);for(let r=0;r<n.length;r++)t[r]=n.charCodeAt(r);return n.length*2}function h(){return new TextDecoder().decode(new Uint8Array(L.buffer,ie,ne()))}function Le(){return new TextDecoder().decode(new Uint8Array(L.buffer,U,rn()))}function K(){return new TextDecoder("utf-16le").decode(new Uint8Array(L.buffer,ie,ne()))}let c={render(e,n){return e===void 0?F():Ke(m(`${e}
2
- ${n}`)),K()},visIxBounds(e){if(nn(oe(e))===0)return null;const n=h().split(",");return{minIx:parseInt(n[0],10),maxIx:parseInt(n[1],10)}},idAtVisIx(e){return T(e)?h():null},visIxOf(e){const n=D(m(e));return n<0?null:n},nextVisId(e){return re(m(e))?h():null},prevVisId(e){return te(m(e))?h():null},visLen(){return H()},merge(e){return Qe(oe(e))!==0},revertTail(e){Xe(e)},contentRange(e){const n=e.startCrdtId?c.nextVisId(e.startCrdtId):c.idAtVisIx(0),t=e.endCrdtId?c.prevVisId(e.endCrdtId):c.idAtVisIx(c.visLen()-1);if(!n||!t)return null;const r=c.visIxOf(n),i=c.visIxOf(t);return r===null||i===null||r>i?null:{startId:n,endId:t}}},g={commonParent(e,n){const t=new Map;for(let r=e;r!==l&&r.parentNode;r=r.parentNode)t.set(r.parentNode,r);for(let r=n;r.parentNode;r=r.parentNode){const i=r.parentNode,o=t.get(i);if(o)return{start:o,end:r,parent:i};if(i===l)break}return null},trackedPrevSibling(e){let n=e.previousSibling;for(;n&&!d.getRange(n);)n=n.previousSibling;return n},trackedNextSibling(e){let n=e.nextSibling;for(;n&&!d.getRange(n);)n=n.nextSibling;return n},nodesForVisRange(e,n){if(d.length===0)return null;const t=d.length-1,r=d.getAtVisIx(Math.max(0,Math.min(e,t))),i=d.getAtVisIx(Math.max(0,Math.min(n,t)));return!r||!i?(C&&console.error("nodesForVisRange: null hit on clamped index",{startIx:e,endIx:n,last:t}),null):g.commonParent(r.node.domNode,i.node.domNode)},fenceposts(e,n,t){return{before:g.trackedPrevSibling(e),after:g.trackedNextSibling(n),parent:t}},captureRegion(e,n,t){const r=d.getRange(e),i=e===n?r:d.getRange(n);return!r||!i?null:{...g.fenceposts(e,n,t),startCrdtId:r[0]>0?c.idAtVisIx(r[0]-1):null,endCrdtId:i[1]<c.visLen()-1?c.idAtVisIx(i[1]+1):null}},captureLocalRegion(e,n,t){const r=d.getRange(e),i=e===n?r:d.getRange(n);return!r||!i?null:{...g.fenceposts(e,n,t),startVisIx:r[0],endVisIx:i[1]}},regionForVisBounds(e){if(!e)return null;const n=c.visLen()-1;if(e.minIx<=0&&e.maxIx>=n)return null;const t=g.nodesForVisRange(Math.max(0,Math.min(e.minIx,n)),Math.max(0,Math.min(e.maxIx,n)));if(!t)return null;const r=g.captureRegion(t.start,t.end,t.parent);if(!r)return null;const i=[r];for(let o=t.parent;o!==l;o=o.parentNode){const s=g.captureRegion(o,o,o.parentNode);if(!s)return null;i.push(s)}return i},resyncRope(e){try{d.update(e.before,e.after,e.parent)}catch(n){console.error("[rope] scoped resync failed \u2014 rebuilding whole rope",n),M()}},htmlBetween(e,n,t){const r=e?e.nextSibling:t.firstChild,i=n?n.previousSibling:t.lastChild;return!r||r===n?"":sn(r,i)},swap(e,n,t,r){pe(()=>{const i=v.createRange();e?i.setStartAfter(e):i.setStart(t,0),n?i.setEndBefore(n):i.setEnd(t,t.childNodes.length),i.deleteContents(),i.insertNode(ln(r,t))})},renderLevels(e){for(const n of e){const t=c.contentRange(n),r=t?c.render(t.startId,t.endId):"";if(n.parent===l||un(r,n.parent)){g.swap(n.before,n.after,n.parent,r),g.resyncRope(n);return}}},fullRender(){const e=c.render();e!==de()&&(b("innerhtml_set",()=>pe(()=>{l.innerHTML=e})),b("rope_rebuild",()=>M()),fe(O))},forceFullRender(){const e=c.render();b("innerhtml_set",()=>pe(()=>{l.innerHTML=e})),b("rope_rebuild",()=>M()),fe(O)}};function sn(e,n){if(e=e??l.firstChild,n=n??l.lastChild,!e||!n)return"";const t=o=>{if(Z(o))return"";if(o.nodeType===3)return Ln(o.data);if(o.nodeType===1){if(ee(o)){let s="";for(const f of o.childNodes)s+=t(f);return s}return $(o.cloneNode(!0))}return""};if(e===n)return t(e);let r="",i=e;for(;i&&(r+=t(i),i!==n);)i=i.nextSibling;return r}function ln(e,n){const t=n.cloneNode(!1);t.innerHTML=e;const r=v.createDocumentFragment();for(;t.firstChild;)r.appendChild(t.firstChild);return r}function un(e,n){const t=n.cloneNode(!1);return t.innerHTML=e,t.innerHTML===e}function cn(e,n){let t=1/0,r=-1/0;for(let i=n;i<e.length;i++){const o=c.visIxBounds(e[i].payload);if(!o)return null;o.minIx<t&&(t=o.minIx),o.maxIx>r&&(r=o.maxIx)}return{minIx:t,maxIx:r}}function fn(e){const n=d;let t=1/0,r=-1/0,i=!1,o=!1;const s=e.some(u=>u.removedNodes.length>0),f=u=>{u.minIx!==void 0&&u.minIx<t&&(t=u.minIx),u.maxIx!==void 0&&u.maxIx>r&&(r=u.maxIx),u.startEdge&&(i=!0),u.endEdge&&(o=!0)};for(const u of e)if(u.type==="childList"){const V=u.target===l;u.previousSibling?f(X(n,u.previousSibling)):V&&(i=!0),u.nextSibling?f(X(n,u.nextSibling)):V&&(o=!0),!V&&(!u.previousSibling||!u.nextSibling)&&f(X(n,u.target))}else f(X(n,u.target));if(t===1/0&&r===-1/0)return null;(i||t===1/0)&&(t=0),(o||r===-1/0)&&(r=n.length-1);const a=g.nodesForVisRange(t,r);if(!a)return null;const x=g.captureLocalRegion(a.start,a.end,a.parent);return x?{...x,structural:s}:null}function le(e,n){const t=n>0?e.getAtVisIx(n-1):null;if(!t)return null;if(t.node.kind==="text"){const o=t.node.domNode;return{node:o,offset:o.data.length}}const r=t.node.domNode,i=r.parentNode;return i?{node:i,offset:Array.from(i.childNodes).indexOf(r)+1}:null}function ue(){const e=p.getSelection();if(!e||!e.rangeCount||!l.contains(e.anchorNode))return null;const n=d.domPosToVisIx(e.anchorNode,e.anchorOffset),t=d.domPosToVisIx(e.focusNode,e.focusOffset);if(n===null||t===null)return null;const r=H();if(n===t&&n>=r){if(r===0||!T(r-1))return null;const V=h();return{anchorId:V,focusId:V,focusInclusive:!0}}let i,o=!1;if(T(n))i=h();else if(n>=r&&r>0)T(r-1),i=h(),o=!0;else return null;if(n===t)return{anchorId:i,focusId:i};let s,f=!1;if(T(t))s=h();else if(t>=r&&r>0)T(r-1),s=h(),f=!0;else return null;let a,x,u;return n<=t?(a=i,x=s,u=f):(a=s,x=i,u=o),{anchorId:a,focusId:x,focusInclusive:u}}let O=null,ce=!1,q=null;function fe(e){if(e){ce=!0;try{const n=H();let t=!1,r=D(m(e.anchorId));if(r<0){t=!0;const x=te(m(e.anchorId))?h():null;r=x?D(m(x)):0}const i=d.visIxToDomPos(r);if(!i)return;if(e.anchorId===e.focusId){if(e.focusInclusive){const a=d.visIxToDomPos(r+1)??le(d,n);if(!a)return;p.getSelection()?.setBaseAndExtent(a.node,a.offset,a.node,a.offset)}else p.getSelection()?.setBaseAndExtent(i.node,i.offset,i.node,i.offset);return}const o=D(m(e.focusId));let s=null,f=!1;if(o>=0)e.focusInclusive?s=d.visIxToDomPos(o+1)??le(d,n):s=d.visIxToDomPos(o);else if(f=!0,re(m(e.focusId))){const x=D(m(h()));s=d.visIxToDomPos(x)}else s=le(d,n);if(!s)return;if(t&&f){p.getSelection()?.setBaseAndExtent(s.node,s.offset,s.node,s.offset);return}if(o>=0&&o<=r){p.getSelection()?.setBaseAndExtent(i.node,i.offset,i.node,i.offset);return}p.getSelection()?.setBaseAndExtent(i.node,i.offset,s.node,s.offset)}finally{ce=!1}}}let S=!1,E=null,J=0,z=!1;function an(){return tn(),h()}function dn(e){const n=e??an();return We(m(n))?{payload:se(),nextCa:Le()}:null}function gn(e){return Ye(m(e))?{payload:se(),nextCa:Le()}:null}function Ne(e,n,t){if(e===n)return;const r=b("dmp_diff",()=>Nn.diff_main(e,n));b("wasm_batch",()=>{qe();let i=t,o=0;for(const[s,f]of r)if(s===0)i+=f.length;else if(s===-1)Je(i,f.length),i+=f.length;else{const a=on(o,f);ze(i,o,a),o+=a}if(Ge()){const s=se();if(Q(xe(s),s),I?.(s),z&&(z=!1,E=null,J=0),C){const f=r.filter(([a])=>a!==0).map(([a,x])=>`${a===-1?"\u2212":"+"}${JSON.stringify(x)}`).join(" ");console.log(`%c[crdt] op committed @ ${t}`,"color:#0a0;font-weight:bold",f)}}})}function ae(e){b("rope_rebuild",e);const n=b("save_cursor",()=>ue());n&&(O=n),Ve("finishSync")}function pn(e){const n=c.idAtVisIx(e.startVisIx),t=c.idAtVisIx(e.endVisIx),r=n&&t?c.render(n,t):"",i=g.htmlBetween(e.before,e.after,e.parent);return r===i?(e.structural&&ae(()=>g.resyncRope(e)),!1):(C&&console.log(`%c[diff] scoped @ startVisIx ${e.startVisIx}`,"color:#59f;font-weight:bold",`
1
+ import{diff_match_patch as Vn}from"diff-match-patch";import An from"dompurify";import $n from"./rogue.wasm.js";import{CrdtErrorKind as M,parseCrdtError as Dn}from"./crdt_error.js";import{buildRope as Ge,defaultSerializeNode as On,defaultOmitNode as Tn,defaultUnwrapNode as En,escapeText as Rn}from"./dom_rope.js";const Pn=new Vn;function Ve(u,I){const y=I?I.cloneNode(!1):document.createElement("div");return y.innerHTML=u,y.innerHTML===u}function _(u,I){if(!globalThis.__rogueProfile)return I();performance.mark(`${u}-start`);const y=I();return performance.mark(`${u}-end`),performance.measure(u,`${u}-start`,`${u}-end`),y}function oe(u,I){if(!I.isConnected)return{};const y=u.getRange(I);if(y)return{minIx:y[0],maxIx:y[1]};let k=I.previousSibling,B=null;for(;k&&!(B=u.getRange(k));)k=k.previousSibling;let O=I.nextSibling,w=null;for(;O&&!(w=u.getRange(O));)O=O.nextSibling;return{...B?{minIx:B[0]}:{startEdge:!0},...w?{maxIx:w[1]}:{endEdge:!0}}}async function Kn({element:u,replicaId:I=Math.floor(Math.random()*65534)+1,onCommit:y,maxOpBytes:k=8*1024*1024,onOpTooLarge:B,onDocTooLarge:O,onError:w,onChange:Ae,paste:$,doc:U=document,win:v=window,serializeNode:ie=On,omitNode:se=Tn,unwrapNode:le=En}){const We=Uint8Array.from(atob($n),e=>e.charCodeAt(0));let L;const{instance:Ye}=await WebAssembly.instantiate(We,{env:{jsLog:(e,n)=>console.log("[wasm]",new TextDecoder().decode(new Uint8Array(L.buffer,e,n)))}}),$e=Ye.exports;L=$e.memory;const{rogue_init:Qe,rogue_in_ptr:Xe,rogue_out_ptr:Ze,rogue_out_len:ue,rogue_render:fe,rogue_render_range:en,rogue_render_at:nn,rogue_render_at_range:tn,rogue_addr_of_range:rn,rogue_begin_batch:on,rogue_hide:sn,rogue_insert:ln,rogue_commit_batch:un,rogue_undo:fn,rogue_redo:cn,rogue_merge:an,rogue_revert_tail:dn,rogue_deinit:gn,rogue_op_bounds:pn,rogue_vis_len:J,rogue_id_at:D,rogue_vis_ix:T,rogue_prev_vis:ce,rogue_next_vis:ae,rogue_head:De,rogue_ca_len:mn,rogue_set_debug_logging:Oe,rogue_reserve:hn}=$e;if(!Qe(I,65536))throw new Error("rogue_init failed \u2014 out of memory allocating the CRDT");const z=Xe(),de=Ze();let g=Ge(u,ie,se,le);function H(){g.update(null,null,u)}let C=!1;try{C=/(?:^|[?&])debug=sync(?:&|$)/.test(globalThis.location?.search??"")}catch{}Oe(C?1:0);const Te=()=>!!globalThis.__rogueForceFullSync;let G="";function h(e){const n=new TextEncoder().encode(e);return new Uint8Array(L.buffer,z,n.length).set(n),n.length}function Ee(e){return new Uint8Array(L.buffer,z,e.byteLength).set(e),e.byteLength}function ge(){const e=ue(),n=new Uint8Array(e);return n.set(new Uint8Array(L.buffer,de,e)),n}function xn(e,n){const t=new Uint16Array(L.buffer,z+e,n.length);for(let r=0;r<n.length;r++)t[r]=n.charCodeAt(r);return n.length*2}function x(){return new TextDecoder().decode(new Uint8Array(L.buffer,de,ue()))}function Re(){return new TextDecoder().decode(new Uint8Array(L.buffer,z,mn()))}function W(){return new TextDecoder("utf-16le").decode(new Uint8Array(L.buffer,de,ue()))}function E(){return Dn(x())}let f={render(e,n){if(!(e===void 0?fe():en(h(`${e}
2
+ ${n}`)))){const r=E();return r?.type===M.unknown_id?null:(console.error("[sync] render failed",r),w?.(r??{type:-1,name:"unknown"}),"")}return W()},renderAt(e,n,t){if(!(n===void 0?nn(h(e)):tn(h(`${e}
3
+ ${n}
4
+ ${t}`)))){const o=E();return o?.type===M.unknown_id||o?.type===M.out_of_bounds?null:(console.error("[sync] renderAt failed",o),w?.(o??{type:-1,name:"unknown"}),"")}return W()},addrOf(e,n){if(e===void 0)return De(),x();if(!rn(h(`${e}
5
+ ${n}`))){const t=E();return t?.type===M.unknown_id||t?.type===M.out_of_bounds||(console.error("[sync] addrOf failed",t),w?.(t??{type:-1,name:"unknown"})),null}return x()},visIxBounds(e){if(pn(Ee(e))===0)return null;const n=x().split(",");return{minIx:parseInt(n[0],10),maxIx:parseInt(n[1],10)}},idAtVisIx(e){return D(e)?x():null},visIxOf(e){const n=T(h(e));return n<0?null:n},nextVisId(e){return ae(h(e))?x():null},prevVisId(e){return ce(h(e))?x():null},visLen(){return J()},merge(e){return an(Ee(e))},revertTail(e){dn(e)},contentRange(e){const n=e.startCrdtId?f.nextVisId(e.startCrdtId):f.idAtVisIx(0),t=e.endCrdtId?f.prevVisId(e.endCrdtId):f.idAtVisIx(f.visLen()-1);if(!n||!t)return null;const r=f.visIxOf(n),o=f.visIxOf(t);return r===null||o===null||r>o?null:{startId:n,endId:t}}},d={commonParent(e,n){const t=new Map;for(let r=e;r!==u&&r.parentNode;r=r.parentNode)t.set(r.parentNode,r);for(let r=n;r.parentNode;r=r.parentNode){const o=r.parentNode,i=t.get(o);if(i)return{start:i,end:r,parent:o};if(o===u)break}return null},trackedPrevSibling(e){let n=e.previousSibling;for(;n&&!g.getRange(n);)n=n.previousSibling;return n},trackedNextSibling(e){let n=e.nextSibling;for(;n&&!g.getRange(n);)n=n.nextSibling;return n},nodesForVisRange(e,n){if(g.length===0)return null;const t=g.length-1,r=g.getAtVisIx(Math.max(0,Math.min(e,t))),o=g.getAtVisIx(Math.max(0,Math.min(n,t)));return!r||!o?(C&&console.error("nodesForVisRange: null hit on clamped index",{startIx:e,endIx:n,last:t}),null):d.commonParent(r.node.domNode,o.node.domNode)},fenceposts(e,n,t){return{before:d.trackedPrevSibling(e),after:d.trackedNextSibling(n),parent:t}},captureRegion(e,n,t){const r=g.getRange(e),o=e===n?r:g.getRange(n);return!r||!o?null:{...d.fenceposts(e,n,t),startCrdtId:r[0]>0?f.idAtVisIx(r[0]-1):null,endCrdtId:o[1]<f.visLen()-1?f.idAtVisIx(o[1]+1):null}},captureLocalRegion(e,n,t){const r=g.getRange(e),o=e===n?r:g.getRange(n);return!r||!o?null:{...d.fenceposts(e,n,t),startVisIx:r[0],endVisIx:o[1]}},regionForVisBounds(e){if(!e)return null;const n=f.visLen()-1;if(e.minIx<=0&&e.maxIx>=n)return null;const t=d.nodesForVisRange(Math.max(0,Math.min(e.minIx,n)),Math.max(0,Math.min(e.maxIx,n)));if(!t)return null;const r=d.captureRegion(t.start,t.end,t.parent);if(!r)return null;const o=[r];for(let i=t.parent;i!==u;i=i.parentNode){const s=d.captureRegion(i,i,i.parentNode);if(!s)return null;o.push(s)}return o},resyncRope(e){try{g.update(e.before,e.after,e.parent)}catch(n){console.error("[rope] scoped resync failed \u2014 rebuilding whole rope",n),H()}},htmlBetween(e,n,t){const r=e?e.nextSibling:t.firstChild,o=n?n.previousSibling:t.lastChild;return!r||r===n?"":In(r,o)},swap(e,n,t,r){_e(()=>{const o=U.createRange();e?o.setStartAfter(e):o.setStart(t,0),n?o.setEndBefore(n):o.setEnd(t,t.childNodes.length),o.deleteContents(),o.insertNode(yn(r,t))})},renderOp(e){for(const n of e){const t=f.contentRange(n),r=t?f.render(t.startId,t.endId):"";if(r!==null&&Ve(r,n.parent))return d.swap(n.before,n.after,n.parent,r),d.resyncRope(n),!0}return!1},fullRender(){const e=f.render()??"";e!==ve()&&(_("innerhtml_set",()=>_e(()=>{u.innerHTML=e})),_("rope_rebuild",()=>H()),xe(R))},forceFullRender(){const e=f.render()??"";_("innerhtml_set",()=>_e(()=>{u.innerHTML=e})),_("rope_rebuild",()=>H()),xe(R)}};function In(e,n){if(e=e??u.firstChild,n=n??u.lastChild,!e||!n)return"";const t=i=>{if(se(i))return"";if(i.nodeType===3)return Rn(i.data);if(i.nodeType===1){if(le(i)){let s="";for(const a of i.childNodes)s+=t(a);return s}return ie(i.cloneNode(!0))}return""};if(e===n)return t(e);let r="",o=e;for(;o&&(r+=t(o),o!==n);)o=o.nextSibling;return r}function yn(e,n){const t=n.cloneNode(!1);t.innerHTML=e;const r=U.createDocumentFragment();for(;t.firstChild;)r.appendChild(t.firstChild);return r}function vn(e,n){let t=1/0,r=-1/0;for(let o=n;o<e.length;o++){const i=f.visIxBounds(e[o].payload);if(!i)return null;i.minIx<t&&(t=i.minIx),i.maxIx>r&&(r=i.maxIx)}return{minIx:t,maxIx:r}}function bn(e){const n=g;let t=1/0,r=-1/0,o=!1,i=!1;const s=e.some(l=>l.removedNodes.length>0),a=l=>{l.minIx!==void 0&&l.minIx<t&&(t=l.minIx),l.maxIx!==void 0&&l.maxIx>r&&(r=l.maxIx),l.startEdge&&(o=!0),l.endEdge&&(i=!0)};for(const l of e)if(l.type==="childList"){const p=l.target===u;l.previousSibling?a(oe(n,l.previousSibling)):p&&(o=!0),l.nextSibling?a(oe(n,l.nextSibling)):p&&(i=!0),!p&&(!l.previousSibling||!l.nextSibling)&&a(oe(n,l.target))}else a(oe(n,l.target));if(t===1/0&&r===-1/0)return null;(o||t===1/0)&&(t=0),(i||r===-1/0)&&(r=n.length-1);const c=d.nodesForVisRange(t,r);if(!c)return null;const m=d.captureLocalRegion(c.start,c.end,c.parent);return m?{...m,structural:s}:null}function pe(e,n){const t=n>0?e.getAtVisIx(n-1):null;if(!t)return null;if(t.node.kind==="text"){const i=t.node.domNode;return{node:i,offset:i.data.length}}const r=t.node.domNode,o=r.parentNode;return o?{node:o,offset:Array.from(o.childNodes).indexOf(r)+1}:null}function me(){const e=v.getSelection();if(!e||!e.rangeCount||!u.contains(e.anchorNode))return null;const n=g.domPosToVisIx(e.anchorNode,e.anchorOffset),t=g.domPosToVisIx(e.focusNode,e.focusOffset);if(n===null||t===null)return null;const r=J();if(n===t&&n>=r){if(r===0||!D(r-1))return null;const p=x();return{anchorId:p,focusId:p,focusInclusive:!0}}let o,i=!1;if(D(n))o=x();else if(n>=r&&r>0)D(r-1),o=x(),i=!0;else return null;if(n===t)return{anchorId:o,focusId:o};let s,a=!1;if(D(t))s=x();else if(t>=r&&r>0)D(r-1),s=x(),a=!0;else return null;let c,m,l;return n<=t?(c=o,m=s,l=a):(c=s,m=o,l=i),{anchorId:c,focusId:m,focusInclusive:l}}let R=null,he=!1,Y=null;function xe(e){if(e){he=!0;try{const n=J();let t=!1,r=T(h(e.anchorId));if(r<0){t=!0;const m=ce(h(e.anchorId))?x():null;r=m?T(h(m)):0}const o=g.visIxToDomPos(r);if(!o)return;if(e.anchorId===e.focusId){if(e.focusInclusive){const c=g.visIxToDomPos(r+1)??pe(g,n);if(!c)return;v.getSelection()?.setBaseAndExtent(c.node,c.offset,c.node,c.offset)}else v.getSelection()?.setBaseAndExtent(o.node,o.offset,o.node,o.offset);return}const i=T(h(e.focusId));let s=null,a=!1;if(i>=0)e.focusInclusive?s=g.visIxToDomPos(i+1)??pe(g,n):s=g.visIxToDomPos(i);else if(a=!0,ae(h(e.focusId))){const m=T(h(x()));s=g.visIxToDomPos(m)}else s=pe(g,n);if(!s)return;if(t&&a){v.getSelection()?.setBaseAndExtent(s.node,s.offset,s.node,s.offset);return}if(i>=0&&i<=r){v.getSelection()?.setBaseAndExtent(o.node,o.offset,o.node,o.offset);return}v.getSelection()?.setBaseAndExtent(o.node,o.offset,s.node,s.offset)}finally{he=!1}}}let b=!1,Q=null,P=null,X=0,Z=!1;function _n(){return De(),x()}function wn(e){const n=e??_n();return fn(h(n))?{payload:ge(),nextCa:Re()}:null}function Sn(e){return cn(h(e))?{payload:ge(),nextCa:Re()}:null}function Pe(e,n,t){if(e===n)return;const r=_("dmp_diff",()=>Pn.diff_main(e,n));let o=0;for(const[s,a]of r)o+=a.length;if(k){let s=0;for(const[a,c]of r)a===1&&(s+=c.length*2);if(s>k)return B?.(s,k),d.forceFullRender(),!1}return _("wasm_batch",()=>{on();let s=t,a=0,c=!1;for(const[l,p]of r)if(l===0)s+=p.length;else if(l===-1){if(!sn(s,p.length)){c=!0;break}s+=p.length}else{const A=xn(a,p);if(!ln(s,a,A)){c=!0;break}a+=A}if(c){const l=E();return console.error("[sync] local edit stamp failed",l),w?.(l??{type:-1,name:"unknown"}),d.forceFullRender(),!1}const m=un();if(m===-1){const l=E();return console.error("[sync] local commit failed",l),l?.type===M.out_of_memory?O?.():w?.(l??{type:-1,name:"unknown"}),d.forceFullRender(),!1}if(m){const l=ge();if(re(Le(l),l),y?.(l),Q={minIx:t,maxIx:t+o},Z&&(Z=!1,P=null,X=0),C){const p=r.filter(([A])=>A!==0).map(([A,Cn])=>`${A===-1?"\u2212":"+"}${JSON.stringify(Cn)}`).join(" ");console.log(`%c[crdt] op committed @ ${t}`,"color:#0a0;font-weight:bold",p)}}return!0})}function Ie(e){_("rope_rebuild",e);const n=_("save_cursor",()=>me());n&&(R=n),Q&&(Ae?.(Q),Q=null),Me("finishSync")}function Ln(e){const n=f.idAtVisIx(e.startVisIx),t=f.idAtVisIx(e.endVisIx),r=n&&t?f.render(n,t):"";if(r===null)return ye(),!1;const o=d.htmlBetween(e.before,e.after,e.parent);return r===o?(e.structural&&Ie(()=>d.resyncRope(e)),!1):(C&&console.log(`%c[diff] scoped @ startVisIx ${e.startVisIx}`,"color:#59f;font-weight:bold",`
3
6
  crdt:`,JSON.stringify(r),`
4
- dom :`,JSON.stringify(i)),Ne(r,i,e.startVisIx),ae(()=>g.resyncRope(e)),!0)}function Ce(){b("full_render",()=>F());const e=b("read_utf16",()=>K()),n=b("clean_innerhtml",()=>de());if(n===e){M();return}Ne(e,n,0),ae(M)}function de(){const e=Re(l,$,Z,ee);return e?e.serialize():""}function Ae(e,n){const t=Math.min(e.length,n.length);for(let r=0;r<t;r++)if(e[r]!==n[r])return r;return e.length===n.length?-1:t}function Ve(e){if(C)try{F();const n=K(),t=de(),r=d?d.serialize():"",i=[];if(n!==t&&i.push("crdt\u2260dom"),r!==t&&i.push("rope\u2260dom"),!i.length){j&&(console.log("%c\u2713 sync resynced","color:#2a2;font-weight:bold"),j="");return}const o=Ae(n!==t?n:r,t),s=`${i.join(",")}@${o}`;if(s===j)return;j=s;const f=a=>o<0?"":JSON.stringify(a.slice(Math.max(0,o-24),o+24));console.error(`%c\u{1F534} SYNC DIVERGENCE @ ${e} [${i.join(", ")}]`,"color:#fff;background:#c00;font-weight:bold;padding:2px 6px"),o>=0&&console.error(`first diff @ vis_ix ${o}`),console.error("crdt near:",f(n)),console.error("dom near:",f(t)),console.error("rope near:",f(r)),console.error("crdt full:",JSON.stringify(n)),console.error("dom full:",JSON.stringify(t)),console.error("rope full:",JSON.stringify(r))}catch(n){console.error(`\u{1F534} sync invariant check threw @ ${e}`,n)}}let P=!1,G=[];function $e(e){e.preventDefault();const n=e.clipboardData?.getData("text/html")??"",t=e.clipboardData?.getData("text/plain")??"",r=n?vn.sanitize(n):t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\n/g,"<br>");if(y&&y.maxBytes&&r.length>y.maxBytes){y.onRejected?.(r.length,y.maxBytes);return}v.execCommand("insertHTML",!1,r)}function Te(){me();const e=dn(E);e&&(E=e.nextCa,B(e.payload),I?.(e.payload),J++)}function ge(){if(me(),E===null||J<=0)return;const e=gn(E);e&&(E=e.nextCa,B(e.payload),I?.(e.payload),J--)}function De(e){(e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="z"?(e.preventDefault(),e.shiftKey?ge():Te()):e.ctrlKey&&e.key.toLowerCase()==="y"&&(e.preventDefault(),ge())}function Oe(){P=!0}function W(){P&&(P=!1,z=!0,Ce(),mn())}function mn(){if(G.length===0)return;const e=G;G=[];for(const n of e)B(n)}let Y=null;function pe(e){e(),Y?.takeRecords()}function me(){const e=Y?.takeRecords();e&&e.length&&Ee(e)}function k(e){if(!e)return"\u2205";if(e.nodeType===3)return`#text${JSON.stringify(e.data.slice(0,24))}`;const n=e,t=n.id?`#${n.id}`:"",r=typeof n.className=="string"&&n.className?`.${n.className.split(/\s+/)[0]}`:"";return`<${n.tagName?.toLowerCase()}${t}${r}>`}function Ee(e){if(S||P)return;const n=we()?null:fn(e);C&&console.log("%c[mut]","color:#a5f;font-weight:bold",e.map(t=>t.type==="childList"?`childList @${k(t.target)} +[${[...t.addedNodes].map(k).join(", ")}] \u2212[${[...t.removedNodes].map(k).join(", ")}]`:`${t.type} @${k(t.target)}${t.attributeName?` (${t.attributeName})`:""}`),"\u2192",n?`region[${n.startVisIx}] ${k(n.before)}\u2026${k(n.after)}`:"null \u2192 full-doc"),b("sync_total",()=>n?pn(n):Ce())}function ke(){ce||l.contains(p.getSelection()?.anchorNode??null)&&(q&&p.cancelAnimationFrame(q),q=p.requestAnimationFrame(()=>{q=null;const e=ue();e&&(O=e)}))}y!==!1&&l.addEventListener("paste",$e),l.addEventListener("keydown",De),l.addEventListener("compositionstart",Oe),l.addEventListener("compositionend",W),l.addEventListener("focusout",W),v.addEventListener("selectionchange",ke);const Me=new p.MutationObserver(Ee);Me.observe(l,{childList:!0,characterData:!0,attributes:!0,subtree:!0}),Y=Me,p.rogue={editorRoot:()=>l,rope:()=>d,visLen:()=>H(),idAt:e=>T(e)?h():null,visIx:e=>{const n=D(m(e));return n<0?null:n},prevVis:e=>te(m(e))?h():null,nextVis:e=>re(m(e))?h():null,saveCursor:()=>ue(),cursorIds:()=>O,render:()=>(F(),K())};const N=[],he=1e4;function xe(e){const n=new DataView(e.buffer,e.byteOffset,e.byteLength);return{replica:n.getUint16(0,!0),seq:n.getUint32(2,!0)}}function Ie(e){return`${e.replica}:${e.seq}`}function ye(e,n){return e.seq-n.seq||e.replica-n.replica}function hn(e){return Ze(oe(e))!==0}function xn(e){if(!e)return be(c.render(),v);const n=e[e.length-1],t=c.contentRange(n);return be(t?c.render(t.startId,t.endId):c.render(),v)}const Pe=new Set;let w=[];const R=new Set;let A=null;function Q(e,n){const t=Ie(e);R.has(t)||(w.push({gid:e,payload:n}),R.add(t),(!A||ye(e,A)>0)&&(A=e))}function ve(){let e=!1;for(let n=N.length-1;n>=0;n--)c.merge(N[n])&&(Q(xe(N[n]),N[n]),N.splice(n,1),e=!0,n=N.length);return e}function In(e){console.error(`[rogue] parked-ops cap (${he}) exceeded \u2014 incoming op dropped. CRDT state is now incomplete; client should reload to re-sync.`,{cap:he,currentParked:N.length,payloadSize:e.byteLength})}function B(e){me();const n=xe(e),t=Ie(n);if(Pe.has(t)||R.has(t))return;const r=S?null:g.regionForVisBounds(c.visIxBounds(e));if(!A||ye(n,A)>0){if(!c.merge(e)){N.length<he?N.push(e):In(e);return}const o=A;Q(n,e);let s=!1;hn(e)&&!xn(r)&&(c.revertTail(1),Pe.add(t),R.delete(t),w.pop(),A=o,s=!0);const f=s||ve();S||(f?g.fullRender():r&&!we()?g.renderLevels(r):g.fullRender())}else{Q(n,e),w.sort((u,V)=>ye(u.gid,V.gid));const o=w.findIndex(u=>u.gid.replica===n.replica&&u.gid.seq===n.seq),s=w.length-o-1,f=s>0&&!S?g.regionForVisBounds(cn(w,o+1)):null;c.revertTail(s);const a=w.splice(o);for(const u of a)R.delete(Ie(u.gid));A=w.length?w[w.length-1].gid:null,S||(f?g.renderLevels(f):g.fullRender());for(const u of a)B(u.payload);const x=ve();!S&&x&&g.fullRender()}S||(fe(O),Ve("mergeOp"))}return{mergeOp(e){if(P){G.push(e);return}B(e)},beginLoad(){S=!0},endLoad(){S&&(S=!1,g.forceFullRender())},undo:Te,redo:ge,drainParked(){ve()&&g.fullRender()},setSyncDebug(e){C=!!e,Se(e?1:0)},markUserEdit(){z=!0},destroy(){y!==!1&&l.removeEventListener("paste",$e),l.removeEventListener("keydown",De),l.removeEventListener("compositionstart",Oe),l.removeEventListener("compositionend",W),l.removeEventListener("focusout",W),v.removeEventListener("selectionchange",ke),Y?.disconnect(),en()}}}export{Tn as createRogueNode,X as nodeSpan,be as roundTrips};
7
+ dom :`,JSON.stringify(o)),Pe(r,o,e.startVisIx)===!1?!1:(Ie(()=>d.resyncRope(e)),!0))}function ye(){const e=_("full_render",()=>f.render()??""),n=_("clean_innerhtml",()=>ve());if(n===e){H();return}Pe(e,n,0)!==!1&&Ie(H)}function ve(){const e=Ge(u,ie,se,le);return e?e.serialize():""}function Fe(e,n){const t=Math.min(e.length,n.length);for(let r=0;r<t;r++)if(e[r]!==n[r])return r;return e.length===n.length?-1:t}function Me(e){if(C)try{fe();const n=W(),t=ve(),r=g?g.serialize():"",o=[];if(n!==t&&o.push("crdt\u2260dom"),r!==t&&o.push("rope\u2260dom"),!o.length){G&&(console.log("%c\u2713 sync resynced","color:#2a2;font-weight:bold"),G="");return}const i=Fe(n!==t?n:r,t),s=`${o.join(",")}@${i}`;if(s===G)return;G=s;const a=c=>i<0?"":JSON.stringify(c.slice(Math.max(0,i-24),i+24));console.error(`%c\u{1F534} SYNC DIVERGENCE @ ${e} [${o.join(", ")}]`,"color:#fff;background:#c00;font-weight:bold;padding:2px 6px"),i>=0&&console.error(`first diff @ vis_ix ${i}`),console.error("crdt near:",a(n)),console.error("dom near:",a(t)),console.error("rope near:",a(r)),console.error("crdt full:",JSON.stringify(n)),console.error("dom full:",JSON.stringify(t)),console.error("rope full:",JSON.stringify(r))}catch(n){console.error(`\u{1F534} sync invariant check threw @ ${e}`,n)}}let K=!1,ee=[];function Be(e){e.preventDefault();const n=e.clipboardData?.getData("text/html")??"",t=e.clipboardData?.getData("text/plain")??"",r=n?An.sanitize(n):t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\n/g,"<br>");if($&&$.maxBytes&&r.length>$.maxBytes){$.onRejected?.(r.length,$.maxBytes);return}U.execCommand("insertHTML",!1,r)}function Ue(){we();const e=wn(P);e&&(P=e.nextCa,q(e.payload),y?.(e.payload),X++)}function be(){if(we(),P===null||X<=0)return;const e=Sn(P);e&&(P=e.nextCa,q(e.payload),y?.(e.payload),X--)}function He(e){(e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="z"?(e.preventDefault(),e.shiftKey?be():Ue()):e.ctrlKey&&e.key.toLowerCase()==="y"&&(e.preventDefault(),be())}function Ke(){K=!0}function ne(){K&&(K=!1,Z=!0,ye(),Nn())}function Nn(){if(ee.length===0)return;const e=ee;ee=[];for(const n of e)q(n)}let te=null;function _e(e){e(),te?.takeRecords()}function we(){const e=te?.takeRecords();e&&e.length&&je(e)}function F(e){if(!e)return"\u2205";if(e.nodeType===3)return`#text${JSON.stringify(e.data.slice(0,24))}`;const n=e,t=n.id?`#${n.id}`:"",r=typeof n.className=="string"&&n.className?`.${n.className.split(/\s+/)[0]}`:"";return`<${n.tagName?.toLowerCase()}${t}${r}>`}function je(e){if(b||K)return;const n=Te()?null:bn(e);C&&console.log("%c[mut]","color:#a5f;font-weight:bold",e.map(t=>t.type==="childList"?`childList @${F(t.target)} +[${[...t.addedNodes].map(F).join(", ")}] \u2212[${[...t.removedNodes].map(F).join(", ")}]`:`${t.type} @${F(t.target)}${t.attributeName?` (${t.attributeName})`:""}`),"\u2192",n?`region[${n.startVisIx}] ${F(n.before)}\u2026${F(n.after)}`:"null \u2192 full-doc"),_("sync_total",()=>n?Ln(n):ye())}function qe(){he||u.contains(v.getSelection()?.anchorNode??null)&&(Y&&v.cancelAnimationFrame(Y),Y=v.requestAnimationFrame(()=>{Y=null;const e=me();e&&(R=e)}))}$!==!1&&u.addEventListener("paste",Be),u.addEventListener("keydown",He),u.addEventListener("compositionstart",Ke),u.addEventListener("compositionend",ne),u.addEventListener("focusout",ne),U.addEventListener("selectionchange",qe);const Je=new v.MutationObserver(je);Je.observe(u,{childList:!0,characterData:!0,attributes:!0,subtree:!0}),te=Je,v.rogue={editorRoot:()=>u,rope:()=>g,visLen:()=>J(),idAt:e=>D(e)?x():null,visIx:e=>{const n=T(h(e));return n<0?null:n},prevVis:e=>ce(h(e))?x():null,nextVis:e=>ae(h(e))?x():null,saveCursor:()=>me(),cursorIds:()=>R,render:()=>(fe(),W())};const N=[],Se=1e4;function Le(e){const n=new DataView(e.buffer,e.byteOffset,e.byteLength);return{replica:n.getUint16(0,!0),seq:n.getUint32(2,!0)}}function Ne(e){return`${e.replica}:${e.seq}`}function ke(e,n){return e.seq-n.seq||e.replica-n.replica}const ze=new Set;let S=[];const j=new Set;let V=null;function re(e,n){const t=Ne(e);j.has(t)||(S.push({gid:e,payload:n}),j.add(t),(!V||ke(e,V)>0)&&(V=e))}function Ce(){let e=!1;for(let n=N.length-1;n>=0;n--)f.merge(N[n])&&(re(Le(N[n]),N[n]),N.splice(n,1),e=!0,n=N.length);return e}function kn(e){console.error(`[rogue] parked-ops cap (${Se}) exceeded \u2014 incoming op dropped. CRDT state is now incomplete; client should reload to re-sync.`,{cap:Se,currentParked:N.length,payloadSize:e.byteLength})}function q(e){we();const n=Le(e),t=Ne(n);if(ze.has(t)||j.has(t))return;const r=b?null:f.visIxBounds(e),o=r?d.regionForVisBounds(r):null;if(!V||ke(n,V)>0){const s=f.merge(e);if(s===0){N.length<Se?N.push(e):kn(e);return}if(s<0){const m=E();console.error("[sync] dropping un-mergeable op",n,m),w?.(m??{type:-1,name:"unknown"});return}const a=V;re(n,e);let c=!1;b?Ve(f.render()??"",u)||(c=!0):o&&!Te()?c=!d.renderOp(o):Ve(f.render()??"",u)?d.fullRender():c=!0,c&&(f.revertTail(1),ze.add(t),j.delete(t),S.pop(),V=a),!b&&Ce()&&d.fullRender()}else{re(n,e),S.sort((p,A)=>ke(p.gid,A.gid));const s=S.findIndex(p=>p.gid.replica===n.replica&&p.gid.seq===n.seq),a=S.length-s-1,c=a>0&&!b?d.regionForVisBounds(vn(S,s+1)):null;f.revertTail(a);const m=S.splice(s);for(const p of m)j.delete(Ne(p.gid));V=S.length?S[S.length-1].gid:null,b||(c?d.renderOp(c):d.fullRender());for(const p of m)q(p.payload);const l=Ce();!b&&l&&d.fullRender()}b||(xe(R),Me("mergeOp"),Ae?.(r))}return{mergeOp(e){if(K){ee.push(e);return}q(e)},beginLoad(){b=!0},endLoad(){b&&(b=!1,d.forceFullRender())},reserve(e){e>0&&hn(e)},undo:Ue,redo:be,drainParked(){Ce()&&d.fullRender()},setSyncDebug(e){C=!!e,Oe(e?1:0)},markUserEdit(){Z=!0},crdt:{visIxOf:e=>f.visIxOf(e),idAtVisIx:e=>f.idAtVisIx(e),nextVisId:e=>f.nextVisId(e),prevVisId:e=>f.prevVisId(e),visLen:()=>f.visLen()},rope:g,destroy(){$!==!1&&u.removeEventListener("paste",Be),u.removeEventListener("keydown",He),u.removeEventListener("compositionstart",Ke),u.removeEventListener("compositionend",ne),u.removeEventListener("focusout",ne),U.removeEventListener("selectionchange",qe),te?.disconnect(),gn()}}}export{Kn as createRogueNode,oe as nodeSpan,Ve as validHtml};
package/index.d.ts CHANGED
@@ -8,4 +8,18 @@ export type { RogueNode, RogueNodeOptions } from './types/sync.js'
8
8
  export { DomRope, buildRope } from './types/dom_rope.js'
9
9
  export type { SerializeNode, OmitNode, UnwrapNode } from './types/dom_rope.js'
10
10
 
11
+ export { createComments, resolveAnchor, anchorForRange } from './types/comments.js'
12
+ export type { Comments, CommentsOptions, ResolvedThread, DomRange, Comment, Reply, CommentsFetcher } from './types/comments.js'
13
+
14
+ export { createSuggestions } from './types/suggestions.js'
15
+ export type { Suggestions, SuggestionsOptions, ResolvedSuggestion, Suggestion } from './types/suggestions.js'
16
+
17
+ export { createEditorSession } from './types/editor-session.js'
18
+ export type { EditorSurface, EditorSessionOptions } from './types/editor-session.js'
19
+
11
20
  export { RogueText } from './types/rogue_text.js'
21
+
22
+ export { CrdtErrorKind, CrdtErrorNames, parseCrdtError } from './types/crdt_error.js'
23
+ export type { CrdtError } from './types/crdt_error.js'
24
+
25
+ export { resolveReplicaId, syncBaseUrlFromWsUrl, persistedReplicaId } from './types/replica.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pointyink/rogue",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "State-of-the-art HTML sync CRDT engine (Fugue CRDT + rope treap) with a DOM sync layer.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -27,6 +27,14 @@
27
27
  "types": "./types/sync-client.d.ts",
28
28
  "default": "./dist/sync-client.js"
29
29
  },
30
+ "./doc-cache": {
31
+ "types": "./types/doc-cache.d.ts",
32
+ "default": "./dist/doc-cache.js"
33
+ },
34
+ "./doc-bus": {
35
+ "types": "./types/doc-bus.d.ts",
36
+ "default": "./dist/doc-bus.js"
37
+ },
30
38
  "./text": {
31
39
  "types": "./types/rogue_text.d.ts",
32
40
  "default": "./dist/rogue_text.js"
@@ -41,13 +49,16 @@
41
49
  "test:watch": "vitest",
42
50
  "build:wasm": "zig build wasm -Doptimize=ReleaseFast && node scripts/emit-wasm.mjs",
43
51
  "build:types": "tsc -p tsconfig.types.json",
44
- "build:dist": "rm -rf dist && esbuild src/index.js src/sync.js src/dom_rope.js src/rogue_text.js src/oplog.js src/sync-client.js src/headless.js src/rogue.wasm.js --minify --outdir=dist --format=esm --platform=neutral",
52
+ "build:dist": "rm -rf dist && esbuild src/index.js src/sync.js src/dom_rope.js src/comments.js src/suggestions.js src/editor-session.js src/crdt_error.js src/replica.js src/checkpoint.js src/rogue_text.js src/oplog.js src/sync-client.js src/doc-cache.js src/doc-bus.js src/headless.js src/rogue.wasm.js --minify --outdir=dist --format=esm --platform=neutral",
45
53
  "prepublishOnly": "npm run build:dist",
46
- "build": "esbuild ../client/main.js --bundle --minify --outfile=../public/app.js --platform=browser --format=esm --alias:@pointyink/rogue=./src --alias:@pointyink/serde=../serde --sourcemap",
47
- "build:tinymce": "esbuild ../tinymce-plugin/plugin.js --bundle --minify --outfile=../public/tinymce-plugin.js --platform=browser --format=esm --alias:@pointyink/rogue=./src --alias:@pointyink/serde=../serde --sourcemap"
54
+ "build": "esbuild ../clients/tinymce/main.js --bundle --minify --outfile=../public/tiny.js --platform=browser --format=esm --alias:@pointyink/rogue=./src --alias:@pointyink/serde=../serde --sourcemap",
55
+ "build:tinymce": "esbuild ../plugins/tinymce/plugin.js --bundle --minify --outfile=../static/tinymce-plugin.js --platform=browser --format=esm --alias:@pointyink/rogue=./src --alias:@pointyink/serde=../serde --sourcemap",
56
+ "build:huge": "esbuild ../clients/hugerte/main.js --bundle --minify --outfile=../public/huge-app.js --platform=browser --format=esm --alias:@pointyink/hugerte=../plugins/hugerte/plugin.js --alias:@pointyink/rogue=./src --alias:@pointyink/serde=../serde --sourcemap",
57
+ "build:hugerte": "esbuild ../plugins/hugerte/plugin.js --bundle --minify --outfile=../static/hugerte-plugin.js --platform=browser --format=esm --alias:@pointyink/rogue=./src --alias:@pointyink/serde=../serde --sourcemap",
58
+ "build:app": "esbuild ../clients/app/main.js --bundle --minify --outfile=../public/app.js --platform=browser --format=esm --alias:@pointyink/rogue=./src --alias:@pointyink/serde=../serde --sourcemap"
48
59
  },
49
60
  "dependencies": {
50
- "@pointyink/serde": "^0.1.0",
61
+ "@pointyink/serde": "^0.2.1",
51
62
  "diff-match-patch": "^1.0.5",
52
63
  "dompurify": "^3.2.6"
53
64
  },
@@ -55,6 +66,7 @@
55
66
  "@types/diff-match-patch": "^1.0.36",
56
67
  "@types/node": "^26.0.1",
57
68
  "esbuild": "^0.25.0",
69
+ "fake-indexeddb": "^6.0.0",
58
70
  "jsdom": "^26.1.0",
59
71
  "typescript": "^6.0.3",
60
72
  "vitest": "^3.1.1"
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Advance a per-replica watermark with an op's group node id (the first 6 bytes
3
+ * are node_id_v0). Mutates and returns `wm`.
4
+ * @param {Record<string, number>} wm
5
+ * @param {Uint8Array} payload
6
+ * @returns {Record<string, number>}
7
+ */
8
+ export function advanceWatermark(wm: Record<string, number>, payload: Uint8Array): Record<string, number>;
9
+ /**
10
+ * Merge an op, then advance the watermark — only on success. If `mergeOp`
11
+ * throws, the watermark is untouched, so the op is re-fetched next sync.
12
+ * @param {(payload: Uint8Array) => void} mergeOp
13
+ * @param {Record<string, number>} wm
14
+ * @param {Uint8Array} payload
15
+ * @returns {Record<string, number>}
16
+ */
17
+ export function mergeAndAdvance(mergeOp: (payload: Uint8Array) => void, wm: Record<string, number>, payload: Uint8Array): Record<string, number>;
18
+ /**
19
+ * Append the watermark as a `since` query param. Empty watermark → unchanged.
20
+ * @param {string} wsUrl
21
+ * @param {Record<string, number>} wm
22
+ * @returns {string}
23
+ */
24
+ export function appendSince(wsUrl: string, wm: Record<string, number>): string;
@@ -0,0 +1,157 @@
1
+ /** @typedef {{ id: string, author_id: string, start_id: string, end_id: string,
2
+ * message: string, resolved: boolean, created_at: string, updated_at: string,
3
+ * replies: Reply[] }} Comment
4
+ * @typedef {{ id: string, author_id: string, message: string, created_at: string }} Reply
5
+ * @typedef {{ startNode: Node, startOffset: number, endNode: Node, endOffset: number }} DomRange
6
+ * @typedef {Comment & { range: DomRange | null }} ResolvedThread
7
+ */
8
+ /**
9
+ * The CRDT/node-id ⇄ vis-index surface a Rogue node exposes for annotations.
10
+ * @typedef {{ visIxOf(id: string): number | null, idAtVisIx(ix: number): string | null,
11
+ * nextVisId(id: string): string | null, prevVisId(id: string): string | null,
12
+ * visLen(): number }} CrdtSurface
13
+ */
14
+ /**
15
+ * The DOM rope surface a Rogue node exposes — vis-index ⇄ DOM-position mapping.
16
+ * @typedef {{ visIxToDomPos(ix: number): { node: Node, offset: number } | null,
17
+ * domPosToVisIx(node: Node, offset: number): number | null,
18
+ * getAtVisIx(ix: number): import('./dom_rope.js').AtIndexResult | null }} RopeSurface
19
+ */
20
+ /**
21
+ * A minimal fetch-like function returning just what this module reads.
22
+ * @typedef {(input: string, init?: RequestInit) => Promise<{
23
+ * ok: boolean, status: number, json: () => Promise<any>,
24
+ * }>} CommentsFetcher
25
+ */
26
+ /**
27
+ * @typedef {{
28
+ * crdt: CrdtSurface,
29
+ * rope: RopeSurface,
30
+ * baseUrl: string,
31
+ * fetchFn?: CommentsFetcher,
32
+ * rerender?: (threads: ResolvedThread[]) => void,
33
+ * onError?: (error: Error) => void,
34
+ * getSelectionRange?: () => DomRange | null,
35
+ * }} CommentsOptions
36
+ */
37
+ /**
38
+ * @typedef {{
39
+ * load: () => Promise<ResolvedThread[]>,
40
+ * threads: () => ResolvedThread[],
41
+ * create: (message: string) => Promise<void>,
42
+ * reply: (commentId: string, message: string) => Promise<void>,
43
+ * update: (commentId: string, patch: { message?: string, resolved?: boolean }) => Promise<void>,
44
+ * remove: (commentId: string) => Promise<void>,
45
+ * handleDocumentChange: (range: { minIx: number, maxIx: number } | null) => void,
46
+ * handleRemoteChange: () => Promise<ResolvedThread[]>,
47
+ * destroy: () => void,
48
+ * }} Comments
49
+ */
50
+ /** The DOM position just past the last character of a non-empty doc.
51
+ * @param {RopeSurface} rope @param {number} visLen
52
+ * @returns {{ node: Node, offset: number } | null} */
53
+ export function endOfDocPos(rope: RopeSurface, visLen: number): {
54
+ node: Node;
55
+ offset: number;
56
+ } | null;
57
+ /** Resolve a node-id anchor to a live DOM range, or null if the anchor is stale
58
+ * (an endpoint was deleted) or the doc is empty.
59
+ * @param {CrdtSurface} crdt @param {RopeSurface} rope
60
+ * @param {string} startId @param {string} endId
61
+ * @returns {DomRange | null} */
62
+ export function resolveAnchor(crdt: CrdtSurface, rope: RopeSurface, startId: string, endId: string): DomRange | null;
63
+ /** Capture a DOM range as a node-id anchor (inclusive start/end char ids), or
64
+ * null for an empty/collapsed range.
65
+ * @param {CrdtSurface} crdt @param {RopeSurface} rope
66
+ * @param {DomRange} range
67
+ * @returns {{ start_id: string, end_id: string } | null} */
68
+ export function anchorForRange(crdt: CrdtSurface, rope: RopeSurface, range: DomRange): {
69
+ start_id: string;
70
+ end_id: string;
71
+ } | null;
72
+ /**
73
+ * @param {CommentsOptions} options
74
+ * @returns {Comments}
75
+ */
76
+ export function createComments({ crdt, rope, baseUrl, fetchFn, rerender, onError, getSelectionRange }: CommentsOptions): Comments;
77
+ export type Comment = {
78
+ id: string;
79
+ author_id: string;
80
+ start_id: string;
81
+ end_id: string;
82
+ message: string;
83
+ resolved: boolean;
84
+ created_at: string;
85
+ updated_at: string;
86
+ replies: Reply[];
87
+ };
88
+ export type Reply = {
89
+ id: string;
90
+ author_id: string;
91
+ message: string;
92
+ created_at: string;
93
+ };
94
+ export type DomRange = {
95
+ startNode: Node;
96
+ startOffset: number;
97
+ endNode: Node;
98
+ endOffset: number;
99
+ };
100
+ export type ResolvedThread = Comment & {
101
+ range: DomRange | null;
102
+ };
103
+ /**
104
+ * The CRDT/node-id ⇄ vis-index surface a Rogue node exposes for annotations.
105
+ */
106
+ export type CrdtSurface = {
107
+ visIxOf(id: string): number | null;
108
+ idAtVisIx(ix: number): string | null;
109
+ nextVisId(id: string): string | null;
110
+ prevVisId(id: string): string | null;
111
+ visLen(): number;
112
+ };
113
+ /**
114
+ * The DOM rope surface a Rogue node exposes — vis-index ⇄ DOM-position mapping.
115
+ */
116
+ export type RopeSurface = {
117
+ visIxToDomPos(ix: number): {
118
+ node: Node;
119
+ offset: number;
120
+ } | null;
121
+ domPosToVisIx(node: Node, offset: number): number | null;
122
+ getAtVisIx(ix: number): import("./dom_rope.js").AtIndexResult | null;
123
+ };
124
+ /**
125
+ * A minimal fetch-like function returning just what this module reads.
126
+ */
127
+ export type CommentsFetcher = (input: string, init?: RequestInit) => Promise<{
128
+ ok: boolean;
129
+ status: number;
130
+ json: () => Promise<any>;
131
+ }>;
132
+ export type CommentsOptions = {
133
+ crdt: CrdtSurface;
134
+ rope: RopeSurface;
135
+ baseUrl: string;
136
+ fetchFn?: CommentsFetcher;
137
+ rerender?: (threads: ResolvedThread[]) => void;
138
+ onError?: (error: Error) => void;
139
+ getSelectionRange?: () => DomRange | null;
140
+ };
141
+ export type Comments = {
142
+ load: () => Promise<ResolvedThread[]>;
143
+ threads: () => ResolvedThread[];
144
+ create: (message: string) => Promise<void>;
145
+ reply: (commentId: string, message: string) => Promise<void>;
146
+ update: (commentId: string, patch: {
147
+ message?: string;
148
+ resolved?: boolean;
149
+ }) => Promise<void>;
150
+ remove: (commentId: string) => Promise<void>;
151
+ handleDocumentChange: (range: {
152
+ minIx: number;
153
+ maxIx: number;
154
+ } | null) => void;
155
+ handleRemoteChange: () => Promise<ResolvedThread[]>;
156
+ destroy: () => void;
157
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @typedef {{ type: number, name: string, desc?: string }} CrdtError
3
+ */
4
+ /**
5
+ * Parse the structured error payload the WASM wrote into out_buf on a failed
6
+ * call (`{"type":N,"name":"...","desc":"..."}`). Returns null if it can't be
7
+ * parsed — treat that as an unknown error.
8
+ * @param {string} payload
9
+ * @returns {CrdtError | null}
10
+ */
11
+ export function parseCrdtError(payload: string): CrdtError | null;
12
+ /** @type {{ readonly [name: string]: number }} */
13
+ export const CrdtErrorKind: {
14
+ readonly [name: string]: number;
15
+ };
16
+ /** Reverse map: type int → human name, for logging. @type {Record<number, string>} */
17
+ export const CrdtErrorNames: Record<number, string>;
18
+ export type CrdtError = {
19
+ type: number;
20
+ name: string;
21
+ desc?: string;
22
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @typedef {{
3
+ * docId: string,
4
+ * onOp: (payload: Uint8Array) => void,
5
+ * }} DocBusOptions
6
+ *
7
+ * @typedef {{
8
+ * broadcastOp: (payload: Uint8Array) => void,
9
+ * destroy: () => void,
10
+ * }} DocBus
11
+ */
12
+ /**
13
+ * @param {DocBusOptions} options
14
+ * @returns {DocBus}
15
+ */
16
+ export function createDocBus({ docId, onOp }: DocBusOptions): DocBus;
17
+ export type DocBusOptions = {
18
+ docId: string;
19
+ onOp: (payload: Uint8Array) => void;
20
+ };
21
+ export type DocBus = {
22
+ broadcastOp: (payload: Uint8Array) => void;
23
+ destroy: () => void;
24
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Append an op to the doc's cached history. Keyed by (replica, seq) from the
3
+ * op's group node id, so re-caching is an idempotent no-op. The op is stored
4
+ * as raw multi_op_v0 bytes (structured-cloned, survives reload unchanged).
5
+ * @param {string} docId
6
+ * @param {Uint8Array} payload multi_op_v0 binary as emitted by the CRDT
7
+ */
8
+ export function cacheOp(docId: string, payload: Uint8Array): Promise<void>;
9
+ /**
10
+ * Return every cached op for a doc, in canonical (seq, replica) order — the
11
+ * order to replay into a fresh CRDT. Missing/partial logs are fine: the
12
+ * watermark is derived from exactly these ops, so the server sends the gap.
13
+ * @param {string} docId
14
+ * @returns {Promise<Uint8Array[]>}
15
+ */
16
+ export function cachedOps(docId: string): Promise<Uint8Array[]>;
17
+ /**
18
+ * Drop a doc's cached history (e.g. on doc delete).
19
+ * @param {string} docId
20
+ */
21
+ export function clearDoc(docId: string): Promise<void>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The editor surface a host adapts for the shared session. Host-specific.
3
+ * @typedef {{
4
+ * element: HTMLElement,
5
+ * doc: Document,
6
+ * win: Window,
7
+ * serializeNode: import('./dom_rope.js').SerializeNode,
8
+ * omitNode: import('./dom_rope.js').OmitNode,
9
+ * unwrapNode: import('./dom_rope.js').UnwrapNode,
10
+ * setMode: (mode: 'design' | 'readonly') => void,
11
+ * getCollapsedSelection: () => { node: Node, offset: number } | null,
12
+ * getSelectionRange: () => import('./comments.js').DomRange | null,
13
+ * onClick: (handler: () => void) => void,
14
+ * }} EditorSurface
15
+ */
16
+ /**
17
+ * @typedef {EditorSurface & {
18
+ * paste?: false | object,
19
+ * wsUrl?: string,
20
+ * docId?: string,
21
+ * getToken?: () => Promise<string>,
22
+ * isNewDoc?: boolean,
23
+ * readOnly?: boolean,
24
+ * onStatus?: (status: import('./sync-client.js').SyncStatus) => void,
25
+ * onProgress?: (progress: number) => void,
26
+ * onReady?: () => void,
27
+ * onCommit?: (payload: Uint8Array) => void,
28
+ * onOpTooLarge?: (bytes: number, limit: number) => void,
29
+ * onDocTooLarge?: () => void,
30
+ * onError?: (error: import('./crdt_error.js').CrdtError) => void,
31
+ * onDocDeleted?: (docId: string) => void,
32
+ * comments?: {
33
+ * render?: (threads: import('./comments.js').ResolvedThread[]) => void,
34
+ * onClick?: (commentId: string) => void,
35
+ * },
36
+ * suggestions?: {
37
+ * render?: (suggestions: import('./suggestions.js').ResolvedSuggestion[]) => void,
38
+ * onClick?: (suggestionId: string) => void,
39
+ * },
40
+ * }} EditorSessionOptions
41
+ */
42
+ /**
43
+ * Stand up a collaborative editing session against an editor host. Returns the
44
+ * engine node and (if enabled) the comments/suggestions APIs.
45
+ *
46
+ * @param {EditorSessionOptions} options
47
+ * @returns {Promise<{ node: import('./sync.js').RogueNode, comments: import('./comments.js').Comments | null, suggestions: import('./suggestions.js').Suggestions | null, deleteDoc: () => void }>}
48
+ */
49
+ export function createEditorSession({ element, doc, win, serializeNode, omitNode, unwrapNode, setMode, getCollapsedSelection, getSelectionRange, onClick, paste, wsUrl, docId, getToken, isNewDoc, readOnly, onStatus, onProgress, onReady, onCommit, onOpTooLarge, onDocTooLarge, onError, onDocDeleted, comments: commentsOpt, suggestions: suggestionsOpt, }: EditorSessionOptions): Promise<{
50
+ node: import("./sync.js").RogueNode;
51
+ comments: import("./comments.js").Comments | null;
52
+ suggestions: import("./suggestions.js").Suggestions | null;
53
+ deleteDoc: () => void;
54
+ }>;
55
+ /**
56
+ * The editor surface a host adapts for the shared session. Host-specific.
57
+ */
58
+ export type EditorSurface = {
59
+ element: HTMLElement;
60
+ doc: Document;
61
+ win: Window;
62
+ serializeNode: import("./dom_rope.js").SerializeNode;
63
+ omitNode: import("./dom_rope.js").OmitNode;
64
+ unwrapNode: import("./dom_rope.js").UnwrapNode;
65
+ setMode: (mode: "design" | "readonly") => void;
66
+ getCollapsedSelection: () => {
67
+ node: Node;
68
+ offset: number;
69
+ } | null;
70
+ getSelectionRange: () => import("./comments.js").DomRange | null;
71
+ onClick: (handler: () => void) => void;
72
+ };
73
+ export type EditorSessionOptions = EditorSurface & {
74
+ paste?: false | object;
75
+ wsUrl?: string;
76
+ docId?: string;
77
+ getToken?: () => Promise<string>;
78
+ isNewDoc?: boolean;
79
+ readOnly?: boolean;
80
+ onStatus?: (status: import("./sync-client.js").SyncStatus) => void;
81
+ onProgress?: (progress: number) => void;
82
+ onReady?: () => void;
83
+ onCommit?: (payload: Uint8Array) => void;
84
+ onOpTooLarge?: (bytes: number, limit: number) => void;
85
+ onDocTooLarge?: () => void;
86
+ onError?: (error: import("./crdt_error.js").CrdtError) => void;
87
+ onDocDeleted?: (docId: string) => void;
88
+ comments?: {
89
+ render?: (threads: import("./comments.js").ResolvedThread[]) => void;
90
+ onClick?: (commentId: string) => void;
91
+ };
92
+ suggestions?: {
93
+ render?: (suggestions: import("./suggestions.js").ResolvedSuggestion[]) => void;
94
+ onClick?: (suggestionId: string) => void;
95
+ };
96
+ };
package/types/oplog.d.ts CHANGED
@@ -13,6 +13,11 @@ export function enqueue(docId: string, id: string, payload: Uint8Array): Promise
13
13
  * @param {string[]} ids
14
14
  */
15
15
  export function ackMany(docId: string, ids: string[]): Promise<void>;
16
+ /**
17
+ * Drop a doc's pending ops (e.g. on doc delete) so they're never re-sent.
18
+ * @param {string} docId
19
+ */
20
+ export function clearDoc(docId: string): Promise<void>;
16
21
  /**
17
22
  * Return all unacked ops for a doc, oldest first.
18
23
  * Uses a range query on the compound key prefix — `[]` upper bound sorts
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Derive the sync host's HTTPS origin from a ws(s) URL like
3
+ * `wss://sync.example.com/ws/<docId>`.
4
+ * @param {string} wsUrl
5
+ * @returns {string}
6
+ */
7
+ export function syncBaseUrlFromWsUrl(wsUrl: string): string;
8
+ /**
9
+ * Read the persisted replica id for a doc, or null if absent/invalid.
10
+ * @param {string} docId
11
+ * @returns {number | null}
12
+ */
13
+ export function persistedReplicaId(docId: string): number | null;
14
+ /**
15
+ * Resolve the replica id to use for a doc.
16
+ *
17
+ * 1. A read-only peer (readOnly) never writes, so it returns an unused random
18
+ * id without any server round-trip — `/docs/{id}/replica` requires write
19
+ * perm and would 403 a viewer.
20
+ * 2. A persisted id (per-doc, localStorage) wins — stable across reloads.
21
+ * 3. A brand-new doc (isNewDoc) uses 1: the client minted the uuid, so it's
22
+ * the only writer and 1 is collision-free — no server round-trip, works
23
+ * offline.
24
+ * 4. Otherwise ask the server to allocate a fresh id.
25
+ *
26
+ * @param {{
27
+ * docId: string,
28
+ * wsUrl: string,
29
+ * getToken: () => Promise<string>,
30
+ * isNewDoc?: boolean,
31
+ * readOnly?: boolean,
32
+ * }} opts
33
+ * @returns {Promise<number>}
34
+ */
35
+ export function resolveReplicaId({ docId, wsUrl, getToken, isNewDoc, readOnly }: {
36
+ docId: string;
37
+ wsUrl: string;
38
+ getToken: () => Promise<string>;
39
+ isNewDoc?: boolean;
40
+ readOnly?: boolean;
41
+ }): Promise<number>;