noteloom 0.3.2 → 0.4.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/README.md +344 -53
- package/bin/noteloom.mjs +139 -0
- package/dist/canvas.cjs +2 -0
- package/dist/canvas.cjs.map +1 -0
- package/dist/canvas.d.ts +2 -0
- package/dist/canvas.js +5 -0
- package/dist/canvas.js.map +1 -0
- package/dist/collab.cjs +2 -0
- package/dist/collab.cjs.map +1 -0
- package/dist/collab.d.ts +19 -0
- package/dist/collab.js +379 -0
- package/dist/collab.js.map +1 -0
- package/dist/comments.cjs +2 -0
- package/dist/comments.cjs.map +1 -0
- package/dist/comments.d.ts +17 -0
- package/dist/comments.js +19 -0
- package/dist/comments.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +392 -32
- package/dist/index.js +3983 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence.cjs +2 -0
- package/dist/persistence.cjs.map +1 -0
- package/dist/persistence.d.ts +11 -0
- package/dist/persistence.js +12 -0
- package/dist/persistence.js.map +1 -0
- package/dist/shared/CommentAvatar-TKqsM8vG.js +53 -0
- package/dist/shared/CommentAvatar-TKqsM8vG.js.map +1 -0
- package/dist/shared/CommentAvatar-V457IgoQ.cjs +2 -0
- package/dist/shared/CommentAvatar-V457IgoQ.cjs.map +1 -0
- package/dist/shared/CommentThreadCard-CAJ3oPdr.cjs +2 -0
- package/dist/shared/CommentThreadCard-CAJ3oPdr.cjs.map +1 -0
- package/dist/shared/CommentThreadCard-Cxf8GwHJ.js +213 -0
- package/dist/shared/CommentThreadCard-Cxf8GwHJ.js.map +1 -0
- package/dist/shared/CommentsPanel-BwVVyMOC.js +76 -0
- package/dist/shared/CommentsPanel-BwVVyMOC.js.map +1 -0
- package/dist/shared/CommentsPanel-CES4uzye.cjs +2 -0
- package/dist/shared/CommentsPanel-CES4uzye.cjs.map +1 -0
- package/dist/shared/VersionHistory-Bg6fufP5.js +1120 -0
- package/dist/shared/VersionHistory-Bg6fufP5.js.map +1 -0
- package/dist/shared/VersionHistory-DELa2Wbq.cjs +12 -0
- package/dist/shared/VersionHistory-DELa2Wbq.cjs.map +1 -0
- package/dist/shared/VoiceListeningIndicator-BOfVmLK0.js +338 -0
- package/dist/shared/VoiceListeningIndicator-BOfVmLK0.js.map +1 -0
- package/dist/shared/VoiceListeningIndicator-DNX8JlQM.cjs +2 -0
- package/dist/shared/VoiceListeningIndicator-DNX8JlQM.cjs.map +1 -0
- package/dist/shared/fieldRegistry-BoYyW3GH.cjs +2 -0
- package/dist/shared/fieldRegistry-BoYyW3GH.cjs.map +1 -0
- package/dist/shared/fieldRegistry-PjJWXFoB.js +223 -0
- package/dist/shared/fieldRegistry-PjJWXFoB.js.map +1 -0
- package/dist/shared/icons-C1_s0jKK.js +4269 -0
- package/dist/shared/icons-C1_s0jKK.js.map +1 -0
- package/dist/shared/icons-DtCsXuU6.cjs +3531 -0
- package/dist/shared/icons-DtCsXuU6.cjs.map +1 -0
- package/dist/shared/index-B9jyyL8u.js +2145 -0
- package/dist/shared/index-B9jyyL8u.js.map +1 -0
- package/dist/shared/index-D9WEXpYM.cjs +2 -0
- package/dist/shared/index-D9WEXpYM.cjs.map +1 -0
- package/dist/shared/indexedDbPersistence-BKlGPFTP.js +112 -0
- package/dist/shared/indexedDbPersistence-BKlGPFTP.js.map +1 -0
- package/dist/shared/indexedDbPersistence-BWJ1h5Vp.cjs +2 -0
- package/dist/shared/indexedDbPersistence-BWJ1h5Vp.cjs.map +1 -0
- package/dist/shared/leafBlockFactory-A9eUhSe-.js +335 -0
- package/dist/shared/leafBlockFactory-A9eUhSe-.js.map +1 -0
- package/dist/shared/leafBlockFactory-Dp382eP8.cjs +2 -0
- package/dist/shared/leafBlockFactory-Dp382eP8.cjs.map +1 -0
- package/dist/shared/serialize-DWEfk0vU.cjs +3 -0
- package/dist/shared/serialize-DWEfk0vU.cjs.map +1 -0
- package/dist/shared/serialize-T9Odwx24.js +86 -0
- package/dist/shared/serialize-T9Odwx24.js.map +1 -0
- package/dist/shared/starter-kit-CY5wzYDf.js +4980 -0
- package/dist/shared/starter-kit-CY5wzYDf.js.map +1 -0
- package/dist/shared/starter-kit-lPN-NpJ5.cjs +27 -0
- package/dist/shared/starter-kit-lPN-NpJ5.cjs.map +1 -0
- package/dist/shared/usePopoverEdgeClamp-CDG26jBM.cjs +2 -0
- package/dist/shared/usePopoverEdgeClamp-CDG26jBM.cjs.map +1 -0
- package/dist/shared/usePopoverEdgeClamp-gmFm0CnI.js +109 -0
- package/dist/shared/usePopoverEdgeClamp-gmFm0CnI.js.map +1 -0
- package/dist/shared/useServiceWorkerUpdate-BduabXv6.js +94 -0
- package/dist/shared/useServiceWorkerUpdate-BduabXv6.js.map +1 -0
- package/dist/shared/useServiceWorkerUpdate-DQboGesi.cjs +2 -0
- package/dist/shared/useServiceWorkerUpdate-DQboGesi.cjs.map +1 -0
- package/dist/starter-kit.cjs +2 -0
- package/dist/starter-kit.cjs.map +1 -0
- package/dist/starter-kit.d.ts +9 -0
- package/dist/starter-kit.js +10 -0
- package/dist/starter-kit.js.map +1 -0
- package/dist/style.css +3529 -3200
- package/dist/versions.cjs +2 -0
- package/dist/versions.cjs.map +1 -0
- package/dist/versions.d.ts +11 -0
- package/dist/versions.js +13 -0
- package/dist/versions.js.map +1 -0
- package/dist/voice.cjs +2 -0
- package/dist/voice.cjs.map +1 -0
- package/dist/voice.d.ts +7 -0
- package/dist/voice.js +8 -0
- package/dist/voice.js.map +1 -0
- package/package.json +159 -92
- package/dist/noteloom.cjs +0 -3209
- package/dist/noteloom.cjs.map +0 -1
- package/dist/noteloom.es.js +0 -14622
- package/dist/noteloom.es.js.map +0 -1
package/bin/noteloom.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// noteloom CLI — tiny, zero-dependency scaffolder for a custom block or inline
|
|
3
|
+
// type. `npx noteloom new block <name>` / `npx noteloom new inline <name>`.
|
|
4
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const HELP = `noteloom — scaffold an extension
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
npx noteloom new block <name> Create <name>/ with a defineBlock() type
|
|
11
|
+
npx noteloom new inline <name> Create <name>/ with a defineInline() type
|
|
12
|
+
|
|
13
|
+
<name> is the type name (lowercase, e.g. "callout", "mention"). Files are
|
|
14
|
+
written under ./<name>/ in the current directory.
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
function pascal(name) {
|
|
18
|
+
return name.replace(/(^|[-_])(\w)/g, (_, __, c) => c.toUpperCase());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function blockFiles(name) {
|
|
22
|
+
const Comp = `${pascal(name)}Block`;
|
|
23
|
+
return {
|
|
24
|
+
[`${name}/index.js`]: `import { defineBlock } from 'noteloom';
|
|
25
|
+
import { ${Comp} } from './${Comp}.jsx';
|
|
26
|
+
|
|
27
|
+
export const ${name}Block = defineBlock({
|
|
28
|
+
name: '${name}',
|
|
29
|
+
component: ${Comp},
|
|
30
|
+
// 'blocks' (holds child blocks) | 'runs' (holds text) | 'void' (neither)
|
|
31
|
+
contentModel: 'void',
|
|
32
|
+
defaultProps: {},
|
|
33
|
+
toPlainText: (block) => '',
|
|
34
|
+
toHTML: (block) => '<div data-type="${name}"></div>',
|
|
35
|
+
slashCommand: {
|
|
36
|
+
label: '${pascal(name)}',
|
|
37
|
+
keywords: ['${name}'],
|
|
38
|
+
run(store, { blockId, runId, sliceStart, sliceEnd }) {
|
|
39
|
+
// erase the "/${name}" query, then insert your block after \`blockId\`.
|
|
40
|
+
// See examples/02-custom-block for a full slashCommand.run().
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
`,
|
|
45
|
+
[`${name}/${Comp}.jsx`]: `import { useBlock, useEditorStore, operations } from 'noteloom';
|
|
46
|
+
|
|
47
|
+
export function ${Comp}({ id }) {
|
|
48
|
+
const store = useEditorStore();
|
|
49
|
+
const block = useBlock(id);
|
|
50
|
+
if (!block) return null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div data-block-id={id} contentEditable={false}>
|
|
54
|
+
${pascal(name)} block
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
`,
|
|
59
|
+
[`${name}/${name}.test.js`]: `import { describe, it, expect } from 'vitest';
|
|
60
|
+
import { ${name}Block } from './index.js';
|
|
61
|
+
|
|
62
|
+
describe('${name}Block', () => {
|
|
63
|
+
it('is a defineBlock() result', () => {
|
|
64
|
+
expect(${name}Block.kind).toBe('block');
|
|
65
|
+
expect(${name}Block.name).toBe('${name}');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function inlineFiles(name) {
|
|
73
|
+
const Comp = `${pascal(name)}Node`;
|
|
74
|
+
return {
|
|
75
|
+
[`${name}/index.js`]: `import { defineInline } from 'noteloom';
|
|
76
|
+
import { ${Comp} } from './${Comp}.jsx';
|
|
77
|
+
|
|
78
|
+
export const ${name}Inline = defineInline({
|
|
79
|
+
name: '${name}',
|
|
80
|
+
component: ${Comp},
|
|
81
|
+
toPlainText: (run) => run.data?.label ?? '',
|
|
82
|
+
toHTML: (run) => \`<span data-inline-type="${name}">\${run.data?.label ?? ''}</span>\`,
|
|
83
|
+
slashCommand: {
|
|
84
|
+
label: '${pascal(name)}',
|
|
85
|
+
keywords: ['${name}'],
|
|
86
|
+
run(store, { blockId, runId, sliceStart, sliceEnd }) {
|
|
87
|
+
// see src/inlineTypes/shared/insertInlineRun.js usage in the built-ins
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
`,
|
|
92
|
+
[`${name}/${Comp}.jsx`]: `import { useRun } from 'noteloom';
|
|
93
|
+
|
|
94
|
+
export function ${Comp}({ id }) {
|
|
95
|
+
const run = useRun(id);
|
|
96
|
+
if (!run) return null;
|
|
97
|
+
return <span contentEditable={false}>{run.data?.label ?? '${name}'}</span>;
|
|
98
|
+
}
|
|
99
|
+
`,
|
|
100
|
+
[`${name}/${name}.test.js`]: `import { describe, it, expect } from 'vitest';
|
|
101
|
+
import { ${name}Inline } from './index.js';
|
|
102
|
+
|
|
103
|
+
describe('${name}Inline', () => {
|
|
104
|
+
it('is a defineInline() result', () => {
|
|
105
|
+
expect(${name}Inline.kind).toBe('inline');
|
|
106
|
+
expect(${name}Inline.isAtomic).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const [, , cmd, kind, name] = process.argv;
|
|
114
|
+
|
|
115
|
+
if (cmd !== 'new' || !['block', 'inline'].includes(kind) || !name) {
|
|
116
|
+
process.stdout.write(HELP);
|
|
117
|
+
process.exit(cmd ? 1 : 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) {
|
|
121
|
+
process.stderr.write(`error: <name> must start lowercase and be alphanumeric (got "${name}")\n`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
if (existsSync(name)) {
|
|
125
|
+
process.stderr.write(`error: ./${name}/ already exists\n`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const files = kind === 'block' ? blockFiles(name) : inlineFiles(name);
|
|
130
|
+
mkdirSync(name, { recursive: true });
|
|
131
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
132
|
+
const path = join(process.cwd(), rel);
|
|
133
|
+
mkdirSync(join(process.cwd(), rel, '..'), { recursive: true });
|
|
134
|
+
writeFileSync(path, content);
|
|
135
|
+
process.stdout.write(` created ${rel}\n`);
|
|
136
|
+
}
|
|
137
|
+
process.stdout.write(
|
|
138
|
+
`\nRegister it: useEditor({ extensions: [...starterKit(), ${name}${kind === 'block' ? 'Block' : 'Inline'}] })\n`,
|
|
139
|
+
);
|
package/dist/canvas.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/dist/canvas.d.ts
ADDED
package/dist/canvas.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/dist/collab.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("./shared/fieldRegistry-BoYyW3GH.cjs"),C=60*60*1e3,P=24*60*60*1e3;function M({store:c,intervalMs:e=C,maxAgeMs:t=P,onPrune:s,onError:o}){const n=setInterval(()=>{try{const a=c.pruneTombstones({maxAgeMs:t});a>0&&(s==null||s(a))}catch(a){o==null||o(a)}},e);return{stop(){clearInterval(n)}}}const h={HELLO:"hello",OP:"op",SYNC_REQUEST:"syncRequest",SYNC_RESPONSE:"syncResponse",PRESENCE:"presence"};function p(c){return JSON.stringify(c)}function m(c){const e=JSON.parse(c);if(!e||typeof e.type!="string")throw new Error("Invalid sync message: missing type");return e}function w(c){return{type:h.HELLO,peerId:c}}function T(c){return{type:h.OP,envelope:c}}function b(){return{type:h.SYNC_REQUEST}}function R(c){return{type:h.SYNC_RESPONSE,doc:c}}function S(c,e){return{type:h.PRESENCE,peerId:c,data:e}}const O="noteloom-sync",N={iceServers:[{urls:"stun:stun.l.google.com:19302"}]},u=16e3,v=1e6,k=256e3;function A(){return`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}class L{constructor({signaling:e,remotePeerId:t,initiator:s,rtcConfig:o=N}){this.remotePeerId=t,this._signaling=e,this._pc=new RTCPeerConnection(o),this._channel=null,this._openListeners=new Set,this._closeListeners=new Set,this._messageListeners=new Set,this._chunkBuffers=new Map,this._sendQueue=[],this._pumping=!1,this._pc.onicecandidate=n=>{n.candidate&&this._signaling.send(t,{kind:"ice",candidate:n.candidate.toJSON()})},s?(this._wireChannel(this._pc.createDataChannel(O)),this._pc.createOffer().then(n=>this._pc.setLocalDescription(n).then(()=>n)).then(n=>this._signaling.send(t,{kind:"offer",sdp:n}))):this._pc.ondatachannel=n=>this._wireChannel(n.channel),this._unsubscribeSignaling=e.onMessage((n,a)=>{n===t&&this._handleSignal(a)})}async _handleSignal(e){if(e.kind==="offer"){await this._pc.setRemoteDescription(e.sdp);const t=await this._pc.createAnswer();await this._pc.setLocalDescription(t),this._signaling.send(this.remotePeerId,{kind:"answer",sdp:t})}else if(e.kind==="answer")await this._pc.setRemoteDescription(e.sdp);else if(e.kind==="ice")try{await this._pc.addIceCandidate(e.candidate)}catch{}}_wireChannel(e){this._channel=e,e.bufferedAmountLowThreshold=k,e.onopen=()=>{for(const t of this._openListeners)t()},e.onclose=()=>{for(const t of this._closeListeners)t()},e.onmessage=t=>this._handleRawMessage(t.data)}_handleRawMessage(e){if(typeof e!="string"||e.length===0)return;const t=e[0];if(t==="S"){for(const l of this._messageListeners)l(e.slice(1));return}if(t!=="C")return;const s=e.slice(1),o=s.indexOf(":"),n=s.indexOf(":",o+1),a=s.indexOf(":",n+1);if(o===-1||n===-1||a===-1)return;const _=s.slice(0,o),y=Number(s.slice(o+1,n)),d=Number(s.slice(n+1,a)),f=s.slice(a+1);let i=this._chunkBuffers.get(_);if(i||(i={total:d,received:new Map},this._chunkBuffers.set(_,i)),i.received.set(y,f),i.received.size<i.total)return;this._chunkBuffers.delete(_);let r="";for(let l=0;l<i.total;l+=1)r+=i.received.get(l);for(const l of this._messageListeners)l(r)}send(e){var t;if(((t=this._channel)==null?void 0:t.readyState)==="open"){if(e.length<=u)this._sendQueue.push(`S${e}`);else{const s=A(),o=Math.ceil(e.length/u);for(let n=0;n<o;n+=1){const a=e.slice(n*u,(n+1)*u);this._sendQueue.push(`C${s}:${n}:${o}:${a}`)}}this._pumpSendQueue()}}async _pumpSendQueue(){if(!this._pumping){this._pumping=!0;try{for(;this._sendQueue.length>0;){if(!this._channel||this._channel.readyState!=="open"){this._sendQueue.length=0;break}if(this._channel.bufferedAmount>v){await this._waitForBufferedAmountLow();continue}const e=this._sendQueue.shift();try{this._channel.send(e)}catch(t){console.warn("[noteloom] failed to send a sync message to peer",this.remotePeerId,t),this._sendQueue.length=0;break}}}finally{this._pumping=!1}}}_waitForBufferedAmountLow(){return new Promise(e=>{if(!this._channel){e();return}const t=()=>{this._channel.removeEventListener("bufferedamountlow",t),e()};this._channel.addEventListener("bufferedamountlow",t)})}onOpen(e){return this._openListeners.add(e),()=>this._openListeners.delete(e)}onClose(e){return this._closeListeners.add(e),()=>this._closeListeners.delete(e)}onMessage(e){return this._messageListeners.add(e),()=>this._messageListeners.delete(e)}close(){var e,t;this._chunkBuffers.clear(),this._sendQueue.length=0,(e=this._unsubscribeSignaling)==null||e.call(this),(t=this._channel)==null||t.close(),this._pc.close()}}const I=100;class D{constructor({history:e,signaling:t,presenceThrottleMs:s=I}){this._history=e,this._store=e.store,this._signaling=t,this._peers=new Map,this._unsubscribeLocal=this._attachLocalBroadcast(),this._localPresence=null,this._remotePresence=new Map,this._presenceListeners=new Set,this._presenceThrottleMs=s,this._presenceThrottleTimer=null,this._presenceDirty=!1}_attachLocalBroadcast(){const e=this._store.applyOperation.bind(this._store);this._pendingEnvelopes=[],this._store.applyOperation=s=>{const o=e(s),n=this._store.getLastEnvelope();return n&&this._pendingEnvelopes.push(n),o};const t={};for(const s of["perform","performBatch","undo","redo"]){const o=this._history[s].bind(this._history);t[s]=o,this._history[s]=(...n)=>{const a=o(...n);return this._flushPendingEnvelopes(),a}}return()=>{for(const s of Object.keys(t))this._history[s]=t[s];this._store.applyOperation=e}}_flushPendingEnvelopes(){const e=this._pendingEnvelopes;this._pendingEnvelopes=[];for(const t of e)this._broadcast(T(t))}_broadcast(e){const t=p(e);for(const s of this._peers.values())s.send(t)}connect(e,{initiator:t}){var o;(o=this._peers.get(e))==null||o.close();const s=new L({signaling:this._signaling,remotePeerId:e,initiator:t});return s.onOpen(()=>{s.send(p(w(this._signaling.localPeerId))),s.send(p(b())),this._localPresence!==null&&s.send(p(S(this._signaling.localPeerId,this._localPresence)))}),s.onMessage(n=>this._handleMessage(s,n)),s.onClose(()=>{this._peers.delete(e),this._remotePresence.delete(e)&&this._notifyPresenceListeners()}),this._peers.set(e,s),s}setLocalPresence(e){this._localPresence=e,this._presenceDirty=!0,!this._presenceThrottleTimer&&(this._flushLocalPresence(),this._presenceThrottleTimer=setTimeout(()=>{this._presenceThrottleTimer=null,this._presenceDirty&&this._flushLocalPresence()},this._presenceThrottleMs))}_flushLocalPresence(){this._presenceDirty=!1,this._broadcast(S(this._signaling.localPeerId,this._localPresence))}getPresence(){return new Map(this._remotePresence)}onPresenceChange(e){return this._presenceListeners.add(e),()=>this._presenceListeners.delete(e)}_notifyPresenceListeners(){const e=this.getPresence();for(const t of this._presenceListeners)t(e)}_handleMessage(e,t){let s;try{s=m(t)}catch{return}s.type===h.OP?(this._store.applyRemoteOperation(s.envelope),this._history.flush()):s.type===h.SYNC_REQUEST?e.send(p(R(this._store.toJSON()))):s.type===h.SYNC_RESPONSE?this._adoptSnapshotIfEmpty(s.doc):s.type===h.PRESENCE&&(this._remotePresence.set(s.peerId,s.data),this._notifyPresenceListeners())}_adoptSnapshotIfEmpty(e){const t=this._store.getRootId();t&&this._store.getBlock(t)||(this._store.blocks=new Map((e.blocks??[]).map(s=>[s.id,s])),this._store.runs=new Map((e.runs??[]).map(s=>[s.id,s])),this._store.rootId=e.rootId??null,this._store._orders=new Map,this._store._notify([...this._store.blocks.keys(),...this._store.runs.keys()]))}disconnect(e){var t;(t=this._peers.get(e))==null||t.close(),this._peers.delete(e)}destroy(){this._unsubscribeLocal(),this._presenceThrottleTimer&&clearTimeout(this._presenceThrottleTimer);for(const e of this._peers.values())e.close();this._peers.clear(),this._remotePresence.clear(),this._presenceListeners.clear()}}function U({url:c,roomId:e,peerId:t,WebSocketImpl:s=WebSocket}){const o=new Set,n=new Set,a=new Set,_=[],y=`${c}${c.includes("?")?"&":"?"}room=${encodeURIComponent(e)}&peerId=${encodeURIComponent(t)}`,d=new s(y);d.addEventListener("open",()=>{for(const{toPeerId:i,payload:r}of _)f(i,r);_.length=0}),d.addEventListener("message",i=>{let r;try{r=JSON.parse(i.data)}catch{return}if(r.type==="roster")for(const l of r.peerIds??[])for(const E of n)E(l);else if(r.type==="peer-joined")for(const l of n)l(r.peerId);else if(r.type==="peer-left")for(const l of a)l(r.peerId);else if(r.type==="signal")for(const l of o)l(r.fromPeerId,r.payload)});function f(i,r){if(d.readyState!==s.OPEN){_.push({toPeerId:i,payload:r});return}d.send(JSON.stringify({type:"signal",toPeerId:i,payload:r}))}return{localPeerId:t,send(i,r){f(i,r)},onMessage(i){return o.add(i),()=>o.delete(i)},onPeerDiscovered(i){return n.add(i),()=>n.delete(i)},onPeerLeft(i){return a.add(i),()=>a.delete(i)},close(){_.length=0,d.close()}}}exports.FieldClockRegistry=g.FieldClockRegistry;exports.HLC=g.HLC;exports.ListCrdtState=g.ListCrdtState;exports.genPeerId=g.genPeerId;exports.CollabSession=D;exports.MESSAGE_TYPE=h;exports.PeerConnection=L;exports.createPeriodicTombstoneGC=M;exports.createWebSocketSignaling=U;exports.decodeMessage=m;exports.encodeMessage=p;
|
|
2
|
+
//# sourceMappingURL=collab.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collab.cjs","sources":["../src/crdt/gc.js","../src/sync/syncProtocol.js","../src/sync/peerConnection.js","../src/sync/CollabSession.js","../src/sync/websocketSignaling.js"],"sourcesContent":["const DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\nconst DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours\n\n/**\n * A ready-made \"just handle it\" wrapper around `store.pruneTombstones()`\n * — runs it on a periodic timer for the lifetime of a session, so a\n * long-running collaborative document doesn't need anyone to remember to\n * call it manually. Purely opt-in (nothing in the store or CollabSession\n * calls this on its own) — construct one if you want it.\n *\n * `store` is anything with `pruneTombstones()` (an `EditorStore`, or a\n * `History` wrapping one). Returns `{ stop }` — call it when the store is\n * no longer in use to clear the timer.\n */\nexport function createPeriodicTombstoneGC({\n store,\n intervalMs = DEFAULT_INTERVAL_MS,\n maxAgeMs = DEFAULT_MAX_AGE_MS,\n onPrune,\n onError,\n}) {\n const timer = setInterval(() => {\n try {\n const removed = store.pruneTombstones({ maxAgeMs });\n if (removed > 0) onPrune?.(removed);\n } catch (err) {\n onError?.(err);\n }\n }, intervalMs);\n\n return {\n stop() {\n clearInterval(timer);\n },\n };\n}\n","export const MESSAGE_TYPE = {\n HELLO: 'hello',\n OP: 'op',\n SYNC_REQUEST: 'syncRequest',\n SYNC_RESPONSE: 'syncResponse',\n PRESENCE: 'presence',\n};\n\nexport function encodeMessage(message) {\n return JSON.stringify(message);\n}\n\nexport function decodeMessage(raw) {\n const message = JSON.parse(raw);\n if (!message || typeof message.type !== 'string')\n throw new Error('Invalid sync message: missing type');\n return message;\n}\n\n/** Sent once right after a data channel opens, so the receiving side knows who it's talking to. Carries no required action yet — reserved for future presence/awareness use. */\nexport function helloMessage(peerId) {\n return { type: MESSAGE_TYPE.HELLO, peerId };\n}\n\n/** Relays one CRDT-native change envelope, as produced by `EditorStore.getLastEnvelope()`. */\nexport function opMessage(envelope) {\n return { type: MESSAGE_TYPE.OP, envelope };\n}\n\n/** \"Send me your full document state\" — sent on first connecting to a peer, and again on every reconnect (no attempt to track/replay only what was missed). */\nexport function syncRequestMessage() {\n return { type: MESSAGE_TYPE.SYNC_REQUEST };\n}\n\n/** A full document snapshot, as produced by `EditorStore.toJSON()`. */\nexport function syncResponseMessage(doc) {\n return { type: MESSAGE_TYPE.SYNC_RESPONSE, doc };\n}\n\n/**\n * Ephemeral \"here's where I am\" broadcast — a cursor/selection position,\n * a display name, a color, whatever the host app wants peers to see.\n * `data` is opaque to the transport layer (CollabSession only relays it,\n * never inspects or interprets it) — deliberately, since what presence\n * *means* is entirely a host-app concern, not something this package has\n * an opinion on. Never persisted, never part of the document CRDT.\n */\nexport function presenceMessage(peerId, data) {\n return { type: MESSAGE_TYPE.PRESENCE, peerId, data };\n}\n","const SYNC_LABEL = 'noteloom-sync';\nconst DEFAULT_RTC_CONFIG = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };\n\n// RTCDataChannel enforces a max-message-size that `send()` throws past --\n// it varies by browser/OS/negotiation (commonly reported anywhere from\n// ~64KB to 256KB) and isn't queryable in advance, so any single logical\n// message (an op envelope carrying an embedded file's data: URL, or a\n// full-document syncResponse snapshot, both of which can easily exceed\n// that) needs to be fragmented rather than assumed to fit. 16KB is\n// conservative -- comfortably under every browser's limit -- in exchange\n// for more individual send() calls on the rare oversized message.\nconst MAX_CHUNK_PAYLOAD = 16000;\n\n// The data channel's OWN outgoing buffer (bufferedAmount) is a separate\n// limit from max-message-size: even correctly-sized individual chunks can\n// overflow the channel's internal send queue if too many are fired\n// synchronously back to back (a large embedded video easily needs\n// thousands of 16KB chunks) -- send() then throws \"send queue is full\".\n// These thresholds pace sending: once bufferedAmount crosses HIGH, further\n// chunks wait for the channel's 'bufferedamountlow' event (fired once it\n// drains back under LOW) before continuing, so outgoing data is throttled\n// to roughly what the channel can actually keep up with.\nconst BUFFERED_AMOUNT_HIGH_WATERMARK = 1_000_000;\nconst BUFFERED_AMOUNT_LOW_WATERMARK = 256_000;\n\nfunction genMessageId() {\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Wraps one WebRTC connection to a single remote peer, using a caller-\n * supplied `SignalingChannel` (see signaling.js) purely to exchange the\n * SDP offer/answer and ICE candidates needed to establish it. Once the\n * data channel opens, no further traffic goes through signaling — the\n * rest of the sync protocol flows directly peer-to-peer over the channel.\n *\n * `initiator` must be true on exactly one side of each pair (whichever\n * peer starts the connection creates the data channel and the offer; the\n * other side waits for both to arrive).\n */\nexport class PeerConnection {\n constructor({ signaling, remotePeerId, initiator, rtcConfig = DEFAULT_RTC_CONFIG }) {\n this.remotePeerId = remotePeerId;\n this._signaling = signaling;\n this._pc = new RTCPeerConnection(rtcConfig);\n this._channel = null;\n this._openListeners = new Set();\n this._closeListeners = new Set();\n this._messageListeners = new Set();\n this._chunkBuffers = new Map(); // messageId -> { total, received: Map<index, string> }\n this._sendQueue = []; // frame strings waiting to go out, drained by _pumpSendQueue\n this._pumping = false;\n\n this._pc.onicecandidate = (event) => {\n // event.candidate is an RTCIceCandidate class instance, not a plain\n // object -- signaling implementations may serialize `message` (JSON\n // over a WebSocket, structured-clone over a BroadcastChannel, etc.),\n // and neither survives a class instance. .toJSON() is the plain,\n // serializable RTCIceCandidateInit shape that addIceCandidate also\n // accepts directly, so no reconstruction is needed on the other end.\n if (event.candidate)\n this._signaling.send(remotePeerId, { kind: 'ice', candidate: event.candidate.toJSON() });\n };\n\n if (initiator) {\n this._wireChannel(this._pc.createDataChannel(SYNC_LABEL));\n this._pc\n .createOffer()\n .then((offer) => this._pc.setLocalDescription(offer).then(() => offer))\n .then((offer) => this._signaling.send(remotePeerId, { kind: 'offer', sdp: offer }));\n } else {\n this._pc.ondatachannel = (event) => this._wireChannel(event.channel);\n }\n\n this._unsubscribeSignaling = signaling.onMessage((fromPeerId, message) => {\n if (fromPeerId !== remotePeerId) return;\n this._handleSignal(message);\n });\n }\n\n async _handleSignal(message) {\n if (message.kind === 'offer') {\n await this._pc.setRemoteDescription(message.sdp);\n const answer = await this._pc.createAnswer();\n await this._pc.setLocalDescription(answer);\n this._signaling.send(this.remotePeerId, { kind: 'answer', sdp: answer });\n } else if (message.kind === 'answer') {\n await this._pc.setRemoteDescription(message.sdp);\n } else if (message.kind === 'ice') {\n try {\n await this._pc.addIceCandidate(message.candidate);\n } catch {\n // A candidate arriving after the connection is already\n // closed/failed is safe to drop rather than surfacing an error.\n }\n }\n }\n\n _wireChannel(channel) {\n this._channel = channel;\n channel.bufferedAmountLowThreshold = BUFFERED_AMOUNT_LOW_WATERMARK;\n channel.onopen = () => {\n for (const cb of this._openListeners) cb();\n };\n channel.onclose = () => {\n for (const cb of this._closeListeners) cb();\n };\n channel.onmessage = (event) => this._handleRawMessage(event.data);\n }\n\n /**\n * Every wire frame carries a 1-character marker so the receiving side\n * can tell a complete message from one fragment of a larger one without\n * touching the actual payload's own framing (which is opaque to this\n * layer — see syncProtocol.js): 'S' for a single, already-small-enough\n * message; 'C' for one piece of a chunked one, reassembled once every\n * piece with the same messageId has arrived.\n */\n _handleRawMessage(raw) {\n if (typeof raw !== 'string' || raw.length === 0) return;\n const marker = raw[0];\n if (marker === 'S') {\n for (const cb of this._messageListeners) cb(raw.slice(1));\n return;\n }\n if (marker !== 'C') return; // unrecognized frame -- ignore rather than crash on a malformed/foreign message\n\n const rest = raw.slice(1);\n const firstColon = rest.indexOf(':');\n const secondColon = rest.indexOf(':', firstColon + 1);\n const thirdColon = rest.indexOf(':', secondColon + 1);\n if (firstColon === -1 || secondColon === -1 || thirdColon === -1) return;\n const messageId = rest.slice(0, firstColon);\n const index = Number(rest.slice(firstColon + 1, secondColon));\n const total = Number(rest.slice(secondColon + 1, thirdColon));\n const piece = rest.slice(thirdColon + 1);\n\n let buffer = this._chunkBuffers.get(messageId);\n if (!buffer) {\n buffer = { total, received: new Map() };\n this._chunkBuffers.set(messageId, buffer);\n }\n buffer.received.set(index, piece);\n if (buffer.received.size < buffer.total) return;\n\n this._chunkBuffers.delete(messageId);\n let full = '';\n for (let i = 0; i < buffer.total; i += 1) full += buffer.received.get(i);\n for (const cb of this._messageListeners) cb(full);\n }\n\n /**\n * Sends a raw string, transparently fragmenting it first if it's too\n * large for one data-channel message (see MAX_CHUNK_PAYLOAD). Enqueues\n * and returns immediately — the actual sending happens asynchronously\n * via _pumpSendQueue, which paces itself against the channel's own\n * backpressure (see BUFFERED_AMOUNT_HIGH/LOW_WATERMARK) so a large\n * payload's thousands of chunks don't overflow the channel's send queue\n * by firing all at once.\n */\n send(raw) {\n if (this._channel?.readyState !== 'open') return;\n if (raw.length <= MAX_CHUNK_PAYLOAD) {\n this._sendQueue.push(`S${raw}`);\n } else {\n const messageId = genMessageId();\n const total = Math.ceil(raw.length / MAX_CHUNK_PAYLOAD);\n for (let index = 0; index < total; index += 1) {\n const piece = raw.slice(index * MAX_CHUNK_PAYLOAD, (index + 1) * MAX_CHUNK_PAYLOAD);\n this._sendQueue.push(`C${messageId}:${index}:${total}:${piece}`);\n }\n }\n this._pumpSendQueue();\n }\n\n /** Drains _sendQueue one frame at a time, pausing whenever the channel's own outgoing buffer is too full rather than firing send() calls faster than the channel can actually flush them. Only one pump loop ever runs per connection (re-entrant calls from send() while a pump is already draining just top up the same queue). */\n async _pumpSendQueue() {\n if (this._pumping) return;\n this._pumping = true;\n try {\n while (this._sendQueue.length > 0) {\n if (!this._channel || this._channel.readyState !== 'open') {\n this._sendQueue.length = 0;\n break;\n }\n if (this._channel.bufferedAmount > BUFFERED_AMOUNT_HIGH_WATERMARK) {\n await this._waitForBufferedAmountLow();\n continue;\n }\n const frame = this._sendQueue.shift();\n try {\n this._channel.send(frame);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.warn('[noteloom] failed to send a sync message to peer', this.remotePeerId, err);\n // A send() failure here (as opposed to the backpressure this\n // loop already avoids) means the channel itself is in trouble\n // (e.g. closing mid-send) -- draining the rest of the queue\n // against it would just fail the same way, so give up on it.\n this._sendQueue.length = 0;\n break;\n }\n }\n } finally {\n this._pumping = false;\n }\n }\n\n _waitForBufferedAmountLow() {\n return new Promise((resolve) => {\n if (!this._channel) {\n resolve();\n return;\n }\n const handler = () => {\n this._channel.removeEventListener('bufferedamountlow', handler);\n resolve();\n };\n this._channel.addEventListener('bufferedamountlow', handler);\n });\n }\n\n onOpen(cb) {\n this._openListeners.add(cb);\n return () => this._openListeners.delete(cb);\n }\n\n onClose(cb) {\n this._closeListeners.add(cb);\n return () => this._closeListeners.delete(cb);\n }\n\n onMessage(cb) {\n this._messageListeners.add(cb);\n return () => this._messageListeners.delete(cb);\n }\n\n close() {\n this._chunkBuffers.clear();\n this._sendQueue.length = 0;\n this._unsubscribeSignaling?.();\n this._channel?.close();\n this._pc.close();\n }\n}\n","import {\n MESSAGE_TYPE,\n encodeMessage,\n decodeMessage,\n helloMessage,\n opMessage,\n syncRequestMessage,\n syncResponseMessage,\n presenceMessage,\n} from './syncProtocol.js';\nimport { PeerConnection } from './peerConnection.js';\n\nconst DEFAULT_PRESENCE_THROTTLE_MS = 100;\n\n/**\n * Wires a History-wrapped EditorStore up to one or more WebRTC peers.\n * Every local write — `history.perform`, `performBatch` (each op in a\n * multi-op batch individually, not just the batch's net effect),\n * `undo`, and `redo` — is broadcast to every connected peer right after\n * it's applied, using the same envelope `EditorStore` already computed\n * for it. Incoming changes are applied via `store.applyRemoteOperation`,\n * which — by construction — never reaches `history`, so remote edits can\n * never be locally undone.\n *\n * A peer connecting for the first time gets a full document snapshot\n * (`syncResponse`) rather than a partial op backlog, and a peer\n * reconnecting after a drop re-requests the same full snapshot rather\n * than trying to track what it might have missed — simple and correct,\n * at the cost of O(document size) traffic per (re)connect. Swapping in an\n * op-log-since-clock resync later is a pure optimization, not a\n * correctness fix, so it's deliberately not attempted here.\n *\n * Also carries ephemeral presence (cursor/selection position, name,\n * color, ...) alongside the document sync — see setLocalPresence/\n * getPresence/onPresenceChange. Presence is entirely separate from the\n * document CRDT: never persisted, never subject to merge conflicts, just\n * \"whatever the last message said,\" rebroadcast to a newly-joining peer\n * on connect and cleared the moment a peer disconnects.\n */\nexport class CollabSession {\n constructor({ history, signaling, presenceThrottleMs = DEFAULT_PRESENCE_THROTTLE_MS }) {\n this._history = history;\n this._store = history.store;\n this._signaling = signaling;\n this._peers = new Map(); // remotePeerId -> PeerConnection\n this._unsubscribeLocal = this._attachLocalBroadcast();\n\n // Ephemeral presence (cursor position, name, color, ...) -- never\n // part of the document CRDT, never persisted, opaque to this class\n // (see presenceMessage's own doc comment for why).\n this._localPresence = null;\n this._remotePresence = new Map(); // remotePeerId -> data\n this._presenceListeners = new Set();\n this._presenceThrottleMs = presenceThrottleMs;\n this._presenceThrottleTimer = null;\n this._presenceDirty = false;\n }\n\n /**\n * Wraps `store.applyOperation` itself — the one chokepoint every local\n * write path funnels through (perform, performBatch's internal loop,\n * undo, redo) — rather than perform/performBatch individually. A\n * multi-op batch (e.g. \"remove this block, insert a replacement\" for an\n * in-place block-type conversion) calls applyOperation once per op but\n * only returns/settles once at the end; broadcasting only\n * `getLastEnvelope()` after the whole batch would silently drop every\n * op's envelope except the last one. Queuing one envelope per\n * applyOperation call and flushing the queue after perform/\n * performBatch/undo/redo -- all of which call applyOperation and\n * nothing else pushes onto the undo stack or replays inverses -- is\n * what makes multi-op batches sync completely instead of partially.\n * Undo/redo are included deliberately: an undo is itself a local write\n * peers should see too (their document should also lose whatever was\n * undone), not just a local-only reversal.\n */\n _attachLocalBroadcast() {\n const originalApplyOperation = this._store.applyOperation.bind(this._store);\n this._pendingEnvelopes = [];\n this._store.applyOperation = (op) => {\n const inverse = originalApplyOperation(op);\n const envelope = this._store.getLastEnvelope();\n if (envelope) this._pendingEnvelopes.push(envelope);\n return inverse;\n };\n\n const originals = {};\n for (const methodName of ['perform', 'performBatch', 'undo', 'redo']) {\n const original = this._history[methodName].bind(this._history);\n originals[methodName] = original;\n this._history[methodName] = (...args) => {\n const result = original(...args);\n this._flushPendingEnvelopes();\n return result;\n };\n }\n\n return () => {\n for (const methodName of Object.keys(originals))\n this._history[methodName] = originals[methodName];\n this._store.applyOperation = originalApplyOperation;\n };\n }\n\n _flushPendingEnvelopes() {\n const envelopes = this._pendingEnvelopes;\n this._pendingEnvelopes = [];\n for (const envelope of envelopes) {\n // Coarse resync ops (setBlockContentIds/replaceRunSpan/setBlockRuns)\n // have no CRDT-safe wire form yet -- getLastEnvelope() is null for\n // those, so they're already filtered out before reaching this queue.\n this._broadcast(opMessage(envelope));\n }\n }\n\n _broadcast(message) {\n const raw = encodeMessage(message);\n for (const peer of this._peers.values()) peer.send(raw);\n }\n\n /** Establishes (or re-establishes) a connection to `remotePeerId`. `initiator` must be true on exactly one side of each pair. */\n connect(remotePeerId, { initiator }) {\n this._peers.get(remotePeerId)?.close();\n const peer = new PeerConnection({ signaling: this._signaling, remotePeerId, initiator });\n peer.onOpen(() => {\n peer.send(encodeMessage(helloMessage(this._signaling.localPeerId)));\n peer.send(encodeMessage(syncRequestMessage()));\n // A peer connecting after we already have presence set (we've been\n // in the room a while, they're just joining) has no way to know it\n // otherwise -- our next actual move would tell them eventually, but\n // there's no reason to make them wait for that.\n if (this._localPresence !== null) {\n peer.send(encodeMessage(presenceMessage(this._signaling.localPeerId, this._localPresence)));\n }\n });\n peer.onMessage((raw) => this._handleMessage(peer, raw));\n peer.onClose(() => {\n // Reconnection itself is a transport-layer concern (re-call connect\n // when the host app's own retry/backoff policy decides to) -- this\n // session just drops the peer on close. A fresh connect() always\n // starts with syncRequest/syncResponse, so there's no dependency on\n // resuming from wherever the dropped connection left off.\n this._peers.delete(remotePeerId);\n if (this._remotePresence.delete(remotePeerId)) this._notifyPresenceListeners();\n });\n this._peers.set(remotePeerId, peer);\n return peer;\n }\n\n /**\n * Updates this peer's own presence data and broadcasts it to everyone\n * connected, throttled (leading + trailing edge, default 100ms) so a\n * continuous stream of updates (mouse-driven selection changes, say)\n * doesn't send a message per pixel of movement — the first call in a\n * quiet period goes out immediately, further calls within the throttle\n * window coalesce into one trailing send of the latest value.\n */\n setLocalPresence(data) {\n this._localPresence = data;\n this._presenceDirty = true;\n if (this._presenceThrottleTimer) return;\n\n this._flushLocalPresence();\n this._presenceThrottleTimer = setTimeout(() => {\n this._presenceThrottleTimer = null;\n if (this._presenceDirty) this._flushLocalPresence();\n }, this._presenceThrottleMs);\n }\n\n _flushLocalPresence() {\n this._presenceDirty = false;\n this._broadcast(presenceMessage(this._signaling.localPeerId, this._localPresence));\n }\n\n /** A snapshot Map of every OTHER currently-known peer's presence data (never includes this peer's own — the caller already has that). */\n getPresence() {\n return new Map(this._remotePresence);\n }\n\n /** Fires whenever any peer's presence data changes, or a peer disconnects (removing their entry). Returns an unsubscribe function. */\n onPresenceChange(cb) {\n this._presenceListeners.add(cb);\n return () => this._presenceListeners.delete(cb);\n }\n\n _notifyPresenceListeners() {\n const snapshot = this.getPresence();\n for (const cb of this._presenceListeners) cb(snapshot);\n }\n\n _handleMessage(peer, raw) {\n let message;\n try {\n message = decodeMessage(raw);\n } catch {\n return; // malformed message from a misbehaving/incompatible peer -- ignore rather than crash the session\n }\n\n if (message.type === MESSAGE_TYPE.OP) {\n this._store.applyRemoteOperation(message.envelope);\n // A remote write can land on the same field a local edit is\n // mid-coalescing on (e.g. both peers typing into the same run).\n // History's coalescing has no visibility into applyRemoteOperation\n // (by design — that path never touches History), so without this,\n // the NEXT local keystroke would still be folded into the batch\n // that started BEFORE the remote write, and a single local undo\n // would revert past it, clobbering the peer's edit. Flushing here\n // closes that batch out, so the next local keystroke starts a new\n // one whose \"undo to\" state correctly includes what just arrived.\n this._history.flush();\n } else if (message.type === MESSAGE_TYPE.SYNC_REQUEST) {\n peer.send(encodeMessage(syncResponseMessage(this._store.toJSON())));\n } else if (message.type === MESSAGE_TYPE.SYNC_RESPONSE) {\n this._adoptSnapshotIfEmpty(message.doc);\n } else if (message.type === MESSAGE_TYPE.PRESENCE) {\n this._remotePresence.set(message.peerId, message.data);\n this._notifyPresenceListeners();\n }\n // HELLO carries no required action yet -- reserved for future use.\n }\n\n /**\n * Adopts a peer's full document wholesale, ONLY when this side has no\n * document of its own yet (the common \"join and get the existing note\"\n * flow). Deliberately does not attempt to merge two independently-\n * created, already-diverged documents together on first contact — that\n * requires reconciling two entirely different id spaces with no shared\n * history, which is a fundamentally different (and unsolved here)\n * problem from merging concurrent edits to the *same* document that\n * both peers already agree is the same document. If both sides already\n * have real, different content when they connect, this is a no-op and\n * the two documents simply stay unsynced until one side is cleared.\n */\n _adoptSnapshotIfEmpty(doc) {\n const rootId = this._store.getRootId();\n if (rootId && this._store.getBlock(rootId)) return;\n this._store.blocks = new Map((doc.blocks ?? []).map((b) => [b.id, b]));\n this._store.runs = new Map((doc.runs ?? []).map((r) => [r.id, r]));\n this._store.rootId = doc.rootId ?? null;\n this._store._orders = new Map(); // reseed lazily from the newly-adopted contentIds, same as a fresh EditorStore(doc) would\n this._store._notify([...this._store.blocks.keys(), ...this._store.runs.keys()]);\n }\n\n disconnect(remotePeerId) {\n this._peers.get(remotePeerId)?.close();\n this._peers.delete(remotePeerId);\n }\n\n destroy() {\n this._unsubscribeLocal();\n if (this._presenceThrottleTimer) clearTimeout(this._presenceThrottleTimer);\n for (const peer of this._peers.values()) peer.close();\n this._peers.clear();\n this._remotePresence.clear();\n this._presenceListeners.clear();\n }\n}\n","/**\n * A `SignalingChannel` (see signaling.js) backed by a plain WebSocket\n * connection to a relay server — usable for any manual/shared-rendezvous\n * setup: a relay running on the same LAN (no internet needed once peers\n * and the relay share a network), one running on a normal cloud host, or\n * anything in between. Uses only the browser's native `WebSocket`, so\n * this adds no runtime dependency to the package.\n *\n * This is a generic tool, not a specific app feature — it doesn't know or\n * care whether the relay is reachable over the internet or only a LAN;\n * that's entirely a property of the `url` the host app supplies. A\n * minimal reference relay server implementing the wire protocol this\n * expects lives in `tools/lan-relay-server/` (Node-only, not part of\n * this package's browser bundle).\n *\n * Wire protocol (JSON messages over the WebSocket):\n * client -> server, on connect: nothing extra needed -- `roomId` and\n * `peerId` are passed as URL query params so the server knows the\n * room/identity before the first message.\n * server -> client: {type: 'roster', peerIds: [...]} -- once, right after connecting: who else is already in the room\n * server -> client: {type: 'peer-joined', peerId} -- another peer joined the same room later\n * server -> client: {type: 'peer-left', peerId} -- a peer disconnected\n * client -> server: {type: 'signal', toPeerId, payload} -- relay this to one specific peer in the same room\n * server -> client: {type: 'signal', fromPeerId, payload} -- a relayed signal addressed to us\n */\nexport function createWebSocketSignaling({ url, roomId, peerId, WebSocketImpl = WebSocket }) {\n const messageHandlers = new Set();\n const peerJoinedHandlers = new Set();\n const peerLeftHandlers = new Set();\n const pendingSends = []; // queued {toPeerId, payload} while the socket is still connecting\n\n const connectUrl = `${url}${url.includes('?') ? '&' : '?'}room=${encodeURIComponent(roomId)}&peerId=${encodeURIComponent(peerId)}`;\n const socket = new WebSocketImpl(connectUrl);\n\n socket.addEventListener('open', () => {\n for (const { toPeerId, payload } of pendingSends) sendSignal(toPeerId, payload);\n pendingSends.length = 0;\n });\n\n socket.addEventListener('message', (event) => {\n let message;\n try {\n message = JSON.parse(event.data);\n } catch {\n return; // malformed frame from an incompatible/misbehaving relay -- ignore rather than crash\n }\n if (message.type === 'roster') {\n for (const otherId of message.peerIds ?? []) {\n for (const cb of peerJoinedHandlers) cb(otherId);\n }\n } else if (message.type === 'peer-joined') {\n for (const cb of peerJoinedHandlers) cb(message.peerId);\n } else if (message.type === 'peer-left') {\n for (const cb of peerLeftHandlers) cb(message.peerId);\n } else if (message.type === 'signal') {\n for (const cb of messageHandlers) cb(message.fromPeerId, message.payload);\n }\n });\n\n function sendSignal(toPeerId, payload) {\n if (socket.readyState !== WebSocketImpl.OPEN) {\n pendingSends.push({ toPeerId, payload });\n return;\n }\n socket.send(JSON.stringify({ type: 'signal', toPeerId, payload }));\n }\n\n return {\n localPeerId: peerId,\n\n send(toPeerId, payload) {\n sendSignal(toPeerId, payload);\n },\n\n onMessage(cb) {\n messageHandlers.add(cb);\n return () => messageHandlers.delete(cb);\n },\n\n /** Not part of the base SignalingChannel contract -- a relay-backed signaling implementation can tell you who's already here (the initial roster) and who joins afterward, unlike the peer-to-peer-only BroadcastChannel backend, which can only hear a live announcement. */\n onPeerDiscovered(cb) {\n peerJoinedHandlers.add(cb);\n return () => peerJoinedHandlers.delete(cb);\n },\n\n /** Not part of the base SignalingChannel contract -- the relay knows when a peer's connection drops, so it can tell everyone else, which a pure BroadcastChannel backend has no way to do. */\n onPeerLeft(cb) {\n peerLeftHandlers.add(cb);\n return () => peerLeftHandlers.delete(cb);\n },\n\n close() {\n pendingSends.length = 0;\n socket.close();\n },\n };\n}\n"],"names":["DEFAULT_INTERVAL_MS","DEFAULT_MAX_AGE_MS","createPeriodicTombstoneGC","store","intervalMs","maxAgeMs","onPrune","onError","timer","removed","err","MESSAGE_TYPE","encodeMessage","message","decodeMessage","raw","helloMessage","peerId","opMessage","envelope","syncRequestMessage","syncResponseMessage","doc","presenceMessage","data","SYNC_LABEL","DEFAULT_RTC_CONFIG","MAX_CHUNK_PAYLOAD","BUFFERED_AMOUNT_HIGH_WATERMARK","BUFFERED_AMOUNT_LOW_WATERMARK","genMessageId","PeerConnection","signaling","remotePeerId","initiator","rtcConfig","event","offer","fromPeerId","answer","channel","cb","marker","rest","firstColon","secondColon","thirdColon","messageId","index","total","piece","buffer","full","i","_a","frame","resolve","handler","_b","DEFAULT_PRESENCE_THROTTLE_MS","CollabSession","history","presenceThrottleMs","originalApplyOperation","op","inverse","originals","methodName","original","args","result","envelopes","peer","snapshot","rootId","b","r","createWebSocketSignaling","url","roomId","WebSocketImpl","messageHandlers","peerJoinedHandlers","peerLeftHandlers","pendingSends","connectUrl","socket","toPeerId","payload","sendSignal","otherId"],"mappings":"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"}
|
package/dist/collab.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// noteloom/collab — types re-exported from the main declaration file so there's
|
|
2
|
+
// a single source of truth while src/index.d.ts stays hand-written.
|
|
3
|
+
export {
|
|
4
|
+
HLC,
|
|
5
|
+
genPeerId,
|
|
6
|
+
ListCrdtState,
|
|
7
|
+
FieldClockRegistry,
|
|
8
|
+
createPeriodicTombstoneGC,
|
|
9
|
+
CollabSession,
|
|
10
|
+
PeerConnection,
|
|
11
|
+
MESSAGE_TYPE,
|
|
12
|
+
encodeMessage,
|
|
13
|
+
decodeMessage,
|
|
14
|
+
createWebSocketSignaling,
|
|
15
|
+
SignalingChannel,
|
|
16
|
+
RemoteOperationEnvelope,
|
|
17
|
+
HlcTimestamp,
|
|
18
|
+
ListCrdtSlot,
|
|
19
|
+
} from './index';
|