@pointyink/rogue 0.1.1 → 0.2.0
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.
- package/dist/checkpoint.js +1 -0
- package/dist/comments.js +1 -0
- package/dist/crdt_error.js +1 -0
- package/dist/doc-bus.js +1 -0
- package/dist/doc-cache.js +1 -0
- package/dist/editor-session.js +1 -0
- package/dist/index.js +1 -1
- package/dist/oplog.js +1 -1
- package/dist/replica.js +1 -0
- package/dist/rogue.wasm.js +1 -1
- package/dist/rogue_text.js +1 -1
- package/dist/sync-client.js +1 -1
- package/dist/sync.js +6 -3
- package/index.d.ts +14 -0
- package/package.json +17 -5
- package/types/checkpoint.d.ts +24 -0
- package/types/comments.d.ts +157 -0
- package/types/crdt_error.d.ts +22 -0
- package/types/doc-bus.d.ts +24 -0
- package/types/doc-cache.d.ts +21 -0
- package/types/editor-session.d.ts +96 -0
- package/types/oplog.d.ts +5 -0
- package/types/replica.d.ts +41 -0
- package/types/suggestions.d.ts +83 -0
- package/types/sync-client.d.ts +13 -1
- package/types/sync.d.ts +77 -10
package/dist/rogue_text.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import a from"./rogue.wasm.js";let
|
|
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};
|
package/dist/sync-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{enqueue as
|
|
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
|
|
2
|
-
${n}`))
|
|
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(
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").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.
|
|
3
|
+
"version": "0.2.0",
|
|
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/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 ../
|
|
47
|
-
"build:tinymce": "esbuild ../tinymce
|
|
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=../public/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=../public/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.
|
|
61
|
+
"@pointyink/serde": "^0.2.0",
|
|
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>;
|