@mdfn/collab 0.1.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 +18 -0
- package/dist/index.cjs +322 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +284 -0
- package/dist/index.js.map +1 -0
- package/package.json +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# `@mdfn/collab`
|
|
2
|
+
|
|
3
|
+
Yjs-backed collaboration for canonical Markdown, validated editorial/asset
|
|
4
|
+
sidecar data, awareness, offline update queues, reconnect flushing, compaction,
|
|
5
|
+
and contract negotiation. Transport remains host-controlled: exchange
|
|
6
|
+
`encodeUpdate()` bytes over an authorized provider and call `applyUpdate()` for
|
|
7
|
+
remote data.
|
|
8
|
+
|
|
9
|
+
Each remote update is size checked and authorized, then applied to an isolated
|
|
10
|
+
candidate document. The candidate must retain the expected document ID, schema
|
|
11
|
+
hash, profile, protocol version, extension set, Markdown, and sidecar validity
|
|
12
|
+
before the live document is changed. Rejections and queue lifecycle events are
|
|
13
|
+
available through the audit callback.
|
|
14
|
+
|
|
15
|
+
Queue compaction merges only unsent dependency-complete updates. It never
|
|
16
|
+
re-encodes the full long-lived `Y.Doc`, whose retained CRDT history can exceed
|
|
17
|
+
the transport limit. If a merged row would exceed `maxUpdateBytes`, the session
|
|
18
|
+
keeps the original incremental rows and flushes them independently.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
Awareness: () => import_awareness.Awareness,
|
|
34
|
+
MDFN_COLLAB_VERSION: () => MDFN_COLLAB_VERSION,
|
|
35
|
+
Y: () => Y,
|
|
36
|
+
createCollaborationSession: () => createCollaborationSession
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
var Y = __toESM(require("yjs"), 1);
|
|
40
|
+
var import_awareness = require("y-protocols/awareness");
|
|
41
|
+
var import_core = require("@mdfn/core");
|
|
42
|
+
function parseSidecar(value, markdownLength) {
|
|
43
|
+
if (value === void 0) return void 0;
|
|
44
|
+
try {
|
|
45
|
+
return (0, import_core.validateMdfnSidecar)(JSON.parse(value), { markdownLength });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`MDFN_COLLAB_SIDECAR_INVALID:${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function changedRange(previous, current) {
|
|
51
|
+
let from = 0;
|
|
52
|
+
while (from < previous.length && from < current.length && previous[from] === current[from]) from += 1;
|
|
53
|
+
let oldEnd = previous.length;
|
|
54
|
+
let newEnd = current.length;
|
|
55
|
+
while (oldEnd > from && newEnd > from && previous[oldEnd - 1] === current[newEnd - 1]) {
|
|
56
|
+
oldEnd -= 1;
|
|
57
|
+
newEnd -= 1;
|
|
58
|
+
}
|
|
59
|
+
return { from, to: oldEnd, inserted: current.slice(from, newEnd) };
|
|
60
|
+
}
|
|
61
|
+
function createCollaborationSession(options) {
|
|
62
|
+
const doc = options.doc ?? new Y.Doc({ guid: options.documentId });
|
|
63
|
+
const ownsAwareness = !options.awareness;
|
|
64
|
+
const awareness = options.awareness ?? new import_awareness.Awareness(doc);
|
|
65
|
+
const source = doc.getText("markdown");
|
|
66
|
+
const sidecar = doc.getMap("sidecar");
|
|
67
|
+
const metadata = doc.getMap("metadata");
|
|
68
|
+
const sharedDocumentInitialized = options.doc !== void 0 && (source.length > 0 || sidecar.size > 0 || metadata.size > 0);
|
|
69
|
+
const localOrigin = Object.freeze({ session: options.user.id });
|
|
70
|
+
let applyingRemote = false;
|
|
71
|
+
let destroyed = false;
|
|
72
|
+
let online = options.online ?? true;
|
|
73
|
+
let nextPendingId = 0;
|
|
74
|
+
let pendingUpdates = [];
|
|
75
|
+
let inFlight = null;
|
|
76
|
+
let flushing = null;
|
|
77
|
+
let remoteApplication = Promise.resolve();
|
|
78
|
+
let documentGeneration = 0;
|
|
79
|
+
const maxDocumentBytes = options.maxDocumentBytes ?? 2 * 1024 * 1024;
|
|
80
|
+
const maxUpdateBytes = options.maxUpdateBytes ?? maxDocumentBytes + Math.max(64 * 1024, Math.ceil(maxDocumentBytes / 16));
|
|
81
|
+
const emitAudit = (event) => {
|
|
82
|
+
options.onAudit?.({ ...event, documentId: options.documentId, userId: options.user.id });
|
|
83
|
+
};
|
|
84
|
+
const expectedSchema = options.controller.getState().schemaHash;
|
|
85
|
+
const expectedDocumentId = options.documentId;
|
|
86
|
+
const expectedProfile = options.profileId ?? "default";
|
|
87
|
+
const expectedProtocol = String(options.protocolVersion ?? 1);
|
|
88
|
+
const expectedExtensions = JSON.stringify(options.controller.extensions.extensions.map((extension) => ({ name: extension.name, version: extension.version })));
|
|
89
|
+
const bindMetadata = (key, expected, code) => {
|
|
90
|
+
const current = metadata.get(key);
|
|
91
|
+
if (current !== void 0 && current !== expected) throw new Error(`${code}:${current}:${expected}`);
|
|
92
|
+
if (current === void 0) metadata.set(key, expected);
|
|
93
|
+
};
|
|
94
|
+
bindMetadata("schemaHash", expectedSchema, "MDFN_COLLAB_SCHEMA_MISMATCH");
|
|
95
|
+
bindMetadata("documentId", expectedDocumentId, "MDFN_COLLAB_DOCUMENT_MISMATCH");
|
|
96
|
+
bindMetadata("profileId", expectedProfile, "MDFN_COLLAB_PROFILE_MISMATCH");
|
|
97
|
+
bindMetadata("protocolVersion", expectedProtocol, "MDFN_COLLAB_PROTOCOL_MISMATCH");
|
|
98
|
+
bindMetadata("extensions", expectedExtensions, "MDFN_COLLAB_EXTENSIONS_MISMATCH");
|
|
99
|
+
const assertContract = (candidate) => {
|
|
100
|
+
const candidateMetadata = candidate.getMap("metadata");
|
|
101
|
+
const schema = candidateMetadata.get("schemaHash");
|
|
102
|
+
if (schema !== expectedSchema) throw new Error(`MDFN_COLLAB_SCHEMA_MISMATCH:${schema ?? "missing"}:${expectedSchema}`);
|
|
103
|
+
if (candidateMetadata.get("documentId") !== expectedDocumentId) throw new Error("MDFN_COLLAB_DOCUMENT_MISMATCH");
|
|
104
|
+
if (candidateMetadata.get("profileId") !== expectedProfile) throw new Error("MDFN_COLLAB_PROFILE_MISMATCH");
|
|
105
|
+
if (candidateMetadata.get("protocolVersion") !== expectedProtocol) throw new Error("MDFN_COLLAB_PROTOCOL_MISMATCH");
|
|
106
|
+
if (candidateMetadata.get("extensions") !== expectedExtensions) throw new Error("MDFN_COLLAB_EXTENSIONS_MISMATCH");
|
|
107
|
+
const markdown = candidate.getText("markdown").toString();
|
|
108
|
+
try {
|
|
109
|
+
options.controller.validateMarkdown(markdown);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`MDFN_COLLAB_MARKDOWN_INVALID:${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
parseSidecar(candidate.getMap("sidecar").get("value"), markdown.length);
|
|
114
|
+
};
|
|
115
|
+
const canonicalJson = (value) => {
|
|
116
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
117
|
+
if (value === null || typeof value !== "object") return value;
|
|
118
|
+
return Object.fromEntries(
|
|
119
|
+
Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, canonicalJson(entry)])
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
const protectedSidecar = (value) => JSON.stringify(canonicalJson({
|
|
123
|
+
comments: value?.comments ?? [],
|
|
124
|
+
suggestions: value?.suggestions ?? [],
|
|
125
|
+
reviewState: value?.reviewState ?? "draft",
|
|
126
|
+
audit: value?.audit ?? []
|
|
127
|
+
}));
|
|
128
|
+
const validateUpdate = async (update, origin) => {
|
|
129
|
+
const baseline = documentGeneration;
|
|
130
|
+
const candidate = new Y.Doc({ guid: expectedDocumentId });
|
|
131
|
+
try {
|
|
132
|
+
Y.applyUpdate(candidate, Y.encodeStateAsUpdate(doc), "validation:baseline");
|
|
133
|
+
Y.applyUpdate(candidate, update, "validation:candidate");
|
|
134
|
+
assertContract(candidate);
|
|
135
|
+
const previousSidecar = parseSidecar(sidecar.get("value"), source.length);
|
|
136
|
+
const candidateSidecar = parseSidecar(candidate.getMap("sidecar").get("value"), candidate.getText("markdown").length);
|
|
137
|
+
if (protectedSidecar(previousSidecar) !== protectedSidecar(candidateSidecar)) {
|
|
138
|
+
if (!options.authorizeSidecarUpdate || !await options.authorizeSidecarUpdate(previousSidecar, candidateSidecar, origin)) {
|
|
139
|
+
throw new Error("MDFN_COLLAB_EDITORIAL_UPDATE_FORBIDDEN");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return baseline;
|
|
143
|
+
} finally {
|
|
144
|
+
candidate.destroy();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
if (!sharedDocumentInitialized) {
|
|
148
|
+
if (source.length === 0) source.insert(0, options.controller.getState().markdown);
|
|
149
|
+
if (!sidecar.has("value") && options.controller.getState().sidecar) sidecar.set("value", JSON.stringify(options.controller.getState().sidecar));
|
|
150
|
+
} else {
|
|
151
|
+
assertContract(doc);
|
|
152
|
+
const state = options.controller.getState();
|
|
153
|
+
const sharedMarkdown = source.toString();
|
|
154
|
+
const sharedSidecar = parseSidecar(sidecar.get("value"), sharedMarkdown.length);
|
|
155
|
+
let initial = new import_core.Transaction().withSource("collab:initial").withMetadata({ addToHistory: false });
|
|
156
|
+
if (sharedMarkdown !== state.markdown) initial = initial.replaceSource(0, state.markdown.length, sharedMarkdown);
|
|
157
|
+
if (JSON.stringify(sharedSidecar) !== JSON.stringify(state.sidecar)) initial = initial.setSidecar(sharedSidecar);
|
|
158
|
+
if (initial.operations.length > 0) options.controller.dispatch(initial);
|
|
159
|
+
}
|
|
160
|
+
awareness.setLocalState({ user: options.user, documentId: options.documentId });
|
|
161
|
+
const trackDocumentGeneration = () => {
|
|
162
|
+
documentGeneration += 1;
|
|
163
|
+
};
|
|
164
|
+
doc.on("afterTransaction", trackDocumentGeneration);
|
|
165
|
+
const unsubscribe = options.controller.subscribe((change) => {
|
|
166
|
+
if (applyingRemote || destroyed) return;
|
|
167
|
+
doc.transact(() => {
|
|
168
|
+
if (change.documentChanged) {
|
|
169
|
+
const current = source.toString();
|
|
170
|
+
const range = changedRange(current, change.current.markdown);
|
|
171
|
+
if (range.to > range.from) source.delete(range.from, range.to - range.from);
|
|
172
|
+
if (range.inserted) source.insert(range.from, range.inserted);
|
|
173
|
+
}
|
|
174
|
+
if (change.sidecarChanged) {
|
|
175
|
+
if (change.current.sidecar) sidecar.set("value", JSON.stringify(change.current.sidecar));
|
|
176
|
+
else sidecar.delete("value");
|
|
177
|
+
}
|
|
178
|
+
}, localOrigin);
|
|
179
|
+
});
|
|
180
|
+
const applySharedState = (transaction) => {
|
|
181
|
+
if (destroyed || transaction.origin === localOrigin) return;
|
|
182
|
+
applyingRemote = true;
|
|
183
|
+
try {
|
|
184
|
+
const state = options.controller.getState();
|
|
185
|
+
const nextSource = source.toString();
|
|
186
|
+
let edit = new import_core.Transaction().withSource("collab:remote").withMetadata({ addToHistory: false });
|
|
187
|
+
if (nextSource !== state.markdown) edit = edit.replaceSource(0, state.markdown.length, nextSource);
|
|
188
|
+
assertContract(doc);
|
|
189
|
+
const nextSidecar = parseSidecar(sidecar.get("value"), nextSource.length);
|
|
190
|
+
if (JSON.stringify(nextSidecar) !== JSON.stringify(state.sidecar)) edit = edit.setSidecar(nextSidecar);
|
|
191
|
+
if (edit.operations.length > 0) options.controller.dispatch(edit);
|
|
192
|
+
} finally {
|
|
193
|
+
applyingRemote = false;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
doc.on("afterTransaction", applySharedState);
|
|
197
|
+
const compactPending = () => {
|
|
198
|
+
const compactable = pendingUpdates.filter((entry) => entry !== inFlight);
|
|
199
|
+
const update = Y.mergeUpdates(compactable.map((entry) => entry.update));
|
|
200
|
+
if (update.byteLength > maxUpdateBytes) {
|
|
201
|
+
throw new RangeError(`MDFN_COLLAB_UPDATE_TOO_LARGE:${maxUpdateBytes}`);
|
|
202
|
+
}
|
|
203
|
+
if (compactable.length === 0) return update;
|
|
204
|
+
const replacement = { id: ++nextPendingId, update };
|
|
205
|
+
pendingUpdates = inFlight ? [inFlight, replacement] : [replacement];
|
|
206
|
+
emitAudit({ type: "compact", byteLength: update.byteLength, pending: pendingUpdates.length });
|
|
207
|
+
return update;
|
|
208
|
+
};
|
|
209
|
+
const flush = async () => {
|
|
210
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
211
|
+
if (!online || !options.sendUpdate || pendingUpdates.length === 0) return;
|
|
212
|
+
if (flushing) return flushing;
|
|
213
|
+
flushing = (async () => {
|
|
214
|
+
while (online && pendingUpdates.length > 0) {
|
|
215
|
+
const entry = pendingUpdates[0];
|
|
216
|
+
inFlight = entry;
|
|
217
|
+
try {
|
|
218
|
+
await options.sendUpdate(entry.update);
|
|
219
|
+
pendingUpdates = pendingUpdates.filter((candidate) => candidate !== entry);
|
|
220
|
+
emitAudit({ type: "flush", byteLength: entry.update.byteLength, pending: pendingUpdates.length });
|
|
221
|
+
} finally {
|
|
222
|
+
if (inFlight === entry) inFlight = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
})().finally(() => {
|
|
226
|
+
flushing = null;
|
|
227
|
+
});
|
|
228
|
+
return flushing;
|
|
229
|
+
};
|
|
230
|
+
const queueLocalUpdate = (update, origin) => {
|
|
231
|
+
if (destroyed || origin !== localOrigin) return;
|
|
232
|
+
pendingUpdates.push({ id: ++nextPendingId, update: update.slice() });
|
|
233
|
+
emitAudit({ type: "local-update", byteLength: update.byteLength, pending: pendingUpdates.length });
|
|
234
|
+
const threshold = options.compactionThresholdBytes ?? 2 * 1024 * 1024;
|
|
235
|
+
if (pendingUpdates.reduce((total, entry) => total + entry.update.byteLength, 0) > threshold) {
|
|
236
|
+
try {
|
|
237
|
+
compactPending();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (!(error instanceof RangeError) || !error.message.startsWith("MDFN_COLLAB_UPDATE_TOO_LARGE:")) throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (online) void flush().catch((error) => {
|
|
243
|
+
online = false;
|
|
244
|
+
emitAudit({ type: "offline", pending: pendingUpdates.length, error: error instanceof Error ? error.message : String(error) });
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
doc.on("update", queueLocalUpdate);
|
|
248
|
+
if (options.sendUpdate) {
|
|
249
|
+
const initial = Y.encodeStateAsUpdate(doc);
|
|
250
|
+
pendingUpdates.push({ id: ++nextPendingId, update: initial });
|
|
251
|
+
emitAudit({ type: "local-update", byteLength: initial.byteLength, pending: pendingUpdates.length });
|
|
252
|
+
if (online) void flush().catch((error) => {
|
|
253
|
+
online = false;
|
|
254
|
+
emitAudit({ type: "offline", pending: pendingUpdates.length, error: error instanceof Error ? error.message : String(error) });
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
doc,
|
|
259
|
+
awareness,
|
|
260
|
+
encodeUpdate: (stateVector) => stateVector ? Y.encodeStateAsUpdate(doc, stateVector) : Y.encodeStateAsUpdate(doc),
|
|
261
|
+
encodeStateVector: () => Y.encodeStateVector(doc),
|
|
262
|
+
async applyUpdate(update, origin = "transport") {
|
|
263
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
264
|
+
const apply = async () => {
|
|
265
|
+
try {
|
|
266
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
267
|
+
if (update.byteLength > maxUpdateBytes) throw new RangeError(`MDFN_COLLAB_UPDATE_TOO_LARGE:${maxUpdateBytes}`);
|
|
268
|
+
if (options.authorizeUpdate && !await options.authorizeUpdate(update, origin)) throw new Error("MDFN_COLLAB_UPDATE_FORBIDDEN");
|
|
269
|
+
for (; ; ) {
|
|
270
|
+
const baseline = await validateUpdate(update, origin);
|
|
271
|
+
if (baseline !== documentGeneration) continue;
|
|
272
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
273
|
+
Y.applyUpdate(doc, update, origin);
|
|
274
|
+
emitAudit({ type: "remote-update", byteLength: update.byteLength });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
emitAudit({ type: "remote-rejected", byteLength: update.byteLength, error: error instanceof Error ? error.message : String(error) });
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const queued = remoteApplication.then(apply, apply);
|
|
283
|
+
remoteApplication = queued.catch(() => void 0);
|
|
284
|
+
return queued;
|
|
285
|
+
},
|
|
286
|
+
setPresence(value) {
|
|
287
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
288
|
+
awareness.setLocalStateField("presence", value);
|
|
289
|
+
},
|
|
290
|
+
getPresence: () => awareness.getStates(),
|
|
291
|
+
isOnline: () => online,
|
|
292
|
+
pendingUpdateCount: () => pendingUpdates.length,
|
|
293
|
+
async setOnline(value) {
|
|
294
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
295
|
+
online = value;
|
|
296
|
+
emitAudit({ type: value ? "online" : "offline", pending: pendingUpdates.length });
|
|
297
|
+
if (value) await flush();
|
|
298
|
+
},
|
|
299
|
+
flush,
|
|
300
|
+
compact: compactPending,
|
|
301
|
+
destroy() {
|
|
302
|
+
if (destroyed) return;
|
|
303
|
+
destroyed = true;
|
|
304
|
+
unsubscribe();
|
|
305
|
+
doc.off("afterTransaction", applySharedState);
|
|
306
|
+
doc.off("afterTransaction", trackDocumentGeneration);
|
|
307
|
+
doc.off("update", queueLocalUpdate);
|
|
308
|
+
pendingUpdates = [];
|
|
309
|
+
awareness.setLocalState(null);
|
|
310
|
+
if (ownsAwareness) awareness.destroy();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
var MDFN_COLLAB_VERSION = "0.1.0";
|
|
315
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
316
|
+
0 && (module.exports = {
|
|
317
|
+
Awareness,
|
|
318
|
+
MDFN_COLLAB_VERSION,
|
|
319
|
+
Y,
|
|
320
|
+
createCollaborationSession
|
|
321
|
+
});
|
|
322
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import * as Y from \"yjs\";\nimport { Awareness } from \"y-protocols/awareness\";\nimport { Transaction, validateMdfnSidecar, type EditorController, type MdfnJsonValue, type MdfnSidecar } from \"@mdfn/core\";\n\nexport interface CollaborationUser {\n readonly id: string;\n readonly name?: string;\n readonly color?: string;\n readonly metadata?: Readonly<Record<string, MdfnJsonValue>>;\n}\n\nexport interface CollaborationSessionOptions {\n readonly controller: EditorController;\n readonly documentId: string;\n readonly user: CollaborationUser;\n readonly doc?: Y.Doc;\n readonly awareness?: Awareness;\n readonly authorizeUpdate?: (update: Uint8Array, origin: unknown) => boolean | Promise<boolean>;\n /** Protected editorial sidecar changes require an authoritative host decision. */\n readonly authorizeSidecarUpdate?: (previous: MdfnSidecar | undefined, next: MdfnSidecar | undefined, origin: unknown) => boolean | Promise<boolean>;\n readonly profileId?: string;\n readonly protocolVersion?: number;\n /** Maximum valid UTF-8 Markdown size; used to derive safe Yjs update limits. */\n readonly maxDocumentBytes?: number;\n readonly maxUpdateBytes?: number;\n readonly online?: boolean;\n readonly sendUpdate?: (update: Uint8Array) => void | Promise<void>;\n readonly compactionThresholdBytes?: number;\n readonly onAudit?: (event: CollaborationAuditEvent) => void;\n}\n\nexport interface CollaborationAuditEvent {\n readonly type: \"local-update\" | \"remote-update\" | \"remote-rejected\" | \"offline\" | \"online\" | \"flush\" | \"compact\";\n readonly documentId: string;\n readonly userId: string;\n readonly byteLength?: number;\n readonly pending?: number;\n readonly error?: string;\n}\n\nfunction parseSidecar(value: string | undefined, markdownLength?: number): MdfnSidecar | undefined {\n if (value === undefined) return undefined;\n try {\n return validateMdfnSidecar(JSON.parse(value), { markdownLength });\n } catch (error) {\n throw new Error(`MDFN_COLLAB_SIDECAR_INVALID:${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\nexport interface CollaborationSession {\n readonly doc: Y.Doc;\n readonly awareness: Awareness;\n encodeUpdate(stateVector?: Uint8Array): Uint8Array;\n encodeStateVector(): Uint8Array;\n applyUpdate(update: Uint8Array, origin?: unknown): Promise<void>;\n setPresence(value: Readonly<Record<string, MdfnJsonValue>>): void;\n getPresence(): ReadonlyMap<number, unknown>;\n isOnline(): boolean;\n pendingUpdateCount(): number;\n setOnline(online: boolean): Promise<void>;\n flush(): Promise<void>;\n compact(): Uint8Array;\n destroy(): void;\n}\n\nfunction changedRange(previous: string, current: string): { from: number; to: number; inserted: string } {\n let from = 0;\n while (from < previous.length && from < current.length && previous[from] === current[from]) from += 1;\n let oldEnd = previous.length;\n let newEnd = current.length;\n while (oldEnd > from && newEnd > from && previous[oldEnd - 1] === current[newEnd - 1]) { oldEnd -= 1; newEnd -= 1; }\n return { from, to: oldEnd, inserted: current.slice(from, newEnd) };\n}\n\nexport function createCollaborationSession(options: CollaborationSessionOptions): CollaborationSession {\n const doc = options.doc ?? new Y.Doc({ guid: options.documentId });\n const ownsAwareness = !options.awareness;\n const awareness = options.awareness ?? new Awareness(doc);\n const source = doc.getText(\"markdown\");\n const sidecar = doc.getMap<string>(\"sidecar\");\n const metadata = doc.getMap<string>(\"metadata\");\n const sharedDocumentInitialized = options.doc !== undefined && (source.length > 0 || sidecar.size > 0 || metadata.size > 0);\n const localOrigin = Object.freeze({ session: options.user.id });\n let applyingRemote = false;\n let destroyed = false;\n let online = options.online ?? true;\n interface PendingUpdate { readonly id: number; readonly update: Uint8Array; }\n let nextPendingId = 0;\n let pendingUpdates: PendingUpdate[] = [];\n let inFlight: PendingUpdate | null = null;\n let flushing: Promise<void> | null = null;\n let remoteApplication: Promise<void> = Promise.resolve();\n let documentGeneration = 0;\n const maxDocumentBytes = options.maxDocumentBytes ?? 2 * 1024 * 1024;\n const maxUpdateBytes = options.maxUpdateBytes ?? maxDocumentBytes + Math.max(64 * 1024, Math.ceil(maxDocumentBytes / 16));\n\n const emitAudit = (event: Omit<CollaborationAuditEvent, \"documentId\" | \"userId\">): void => {\n options.onAudit?.({ ...event, documentId: options.documentId, userId: options.user.id });\n };\n\n const expectedSchema = options.controller.getState().schemaHash;\n const expectedDocumentId = options.documentId;\n const expectedProfile = options.profileId ?? \"default\";\n const expectedProtocol = String(options.protocolVersion ?? 1);\n const expectedExtensions = JSON.stringify(options.controller.extensions.extensions.map((extension) => ({ name: extension.name, version: extension.version })));\n const bindMetadata = (key: string, expected: string, code: string): void => {\n const current = metadata.get(key);\n if (current !== undefined && current !== expected) throw new Error(`${code}:${current}:${expected}`);\n if (current === undefined) metadata.set(key, expected);\n };\n bindMetadata(\"schemaHash\", expectedSchema, \"MDFN_COLLAB_SCHEMA_MISMATCH\");\n bindMetadata(\"documentId\", expectedDocumentId, \"MDFN_COLLAB_DOCUMENT_MISMATCH\");\n bindMetadata(\"profileId\", expectedProfile, \"MDFN_COLLAB_PROFILE_MISMATCH\");\n bindMetadata(\"protocolVersion\", expectedProtocol, \"MDFN_COLLAB_PROTOCOL_MISMATCH\");\n bindMetadata(\"extensions\", expectedExtensions, \"MDFN_COLLAB_EXTENSIONS_MISMATCH\");\n\n const assertContract = (candidate: Y.Doc): void => {\n const candidateMetadata = candidate.getMap<string>(\"metadata\");\n const schema = candidateMetadata.get(\"schemaHash\");\n if (schema !== expectedSchema) throw new Error(`MDFN_COLLAB_SCHEMA_MISMATCH:${schema ?? \"missing\"}:${expectedSchema}`);\n if (candidateMetadata.get(\"documentId\") !== expectedDocumentId) throw new Error(\"MDFN_COLLAB_DOCUMENT_MISMATCH\");\n if (candidateMetadata.get(\"profileId\") !== expectedProfile) throw new Error(\"MDFN_COLLAB_PROFILE_MISMATCH\");\n if (candidateMetadata.get(\"protocolVersion\") !== expectedProtocol) throw new Error(\"MDFN_COLLAB_PROTOCOL_MISMATCH\");\n if (candidateMetadata.get(\"extensions\") !== expectedExtensions) throw new Error(\"MDFN_COLLAB_EXTENSIONS_MISMATCH\");\n const markdown = candidate.getText(\"markdown\").toString();\n try {\n options.controller.validateMarkdown(markdown);\n } catch (error) {\n throw new Error(`MDFN_COLLAB_MARKDOWN_INVALID:${error instanceof Error ? error.message : String(error)}`);\n }\n parseSidecar(candidate.getMap<string>(\"sidecar\").get(\"value\"), markdown.length);\n };\n\n const canonicalJson = (value: unknown): unknown => {\n if (Array.isArray(value)) return value.map(canonicalJson);\n if (value === null || typeof value !== \"object\") return value;\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)\n .map(([key, entry]) => [key, canonicalJson(entry)]),\n );\n };\n const protectedSidecar = (value: MdfnSidecar | undefined): string => JSON.stringify(canonicalJson({\n comments: value?.comments ?? [],\n suggestions: value?.suggestions ?? [],\n reviewState: value?.reviewState ?? \"draft\",\n audit: value?.audit ?? [],\n }));\n\n const validateUpdate = async (update: Uint8Array, origin: unknown): Promise<number> => {\n const baseline = documentGeneration;\n const candidate = new Y.Doc({ guid: expectedDocumentId });\n try {\n Y.applyUpdate(candidate, Y.encodeStateAsUpdate(doc), \"validation:baseline\");\n Y.applyUpdate(candidate, update, \"validation:candidate\");\n assertContract(candidate);\n const previousSidecar = parseSidecar(sidecar.get(\"value\"), source.length);\n const candidateSidecar = parseSidecar(candidate.getMap<string>(\"sidecar\").get(\"value\"), candidate.getText(\"markdown\").length);\n if (protectedSidecar(previousSidecar) !== protectedSidecar(candidateSidecar)) {\n if (!options.authorizeSidecarUpdate || !(await options.authorizeSidecarUpdate(previousSidecar, candidateSidecar, origin))) {\n throw new Error(\"MDFN_COLLAB_EDITORIAL_UPDATE_FORBIDDEN\");\n }\n }\n return baseline;\n } finally {\n candidate.destroy();\n }\n };\n\n if (!sharedDocumentInitialized) {\n if (source.length === 0) source.insert(0, options.controller.getState().markdown);\n if (!sidecar.has(\"value\") && options.controller.getState().sidecar) sidecar.set(\"value\", JSON.stringify(options.controller.getState().sidecar));\n } else {\n assertContract(doc);\n const state = options.controller.getState();\n const sharedMarkdown = source.toString();\n const sharedSidecar = parseSidecar(sidecar.get(\"value\"), sharedMarkdown.length);\n let initial = new Transaction().withSource(\"collab:initial\").withMetadata({ addToHistory: false });\n if (sharedMarkdown !== state.markdown) initial = initial.replaceSource(0, state.markdown.length, sharedMarkdown);\n if (JSON.stringify(sharedSidecar) !== JSON.stringify(state.sidecar)) initial = initial.setSidecar(sharedSidecar);\n if (initial.operations.length > 0) options.controller.dispatch(initial);\n }\n\n awareness.setLocalState({ user: options.user, documentId: options.documentId });\n\n const trackDocumentGeneration = (): void => { documentGeneration += 1; };\n doc.on(\"afterTransaction\", trackDocumentGeneration);\n\n const unsubscribe = options.controller.subscribe((change) => {\n if (applyingRemote || destroyed) return;\n doc.transact(() => {\n if (change.documentChanged) {\n const current = source.toString();\n const range = changedRange(current, change.current.markdown);\n if (range.to > range.from) source.delete(range.from, range.to - range.from);\n if (range.inserted) source.insert(range.from, range.inserted);\n }\n if (change.sidecarChanged) {\n if (change.current.sidecar) sidecar.set(\"value\", JSON.stringify(change.current.sidecar));\n else sidecar.delete(\"value\");\n }\n }, localOrigin);\n });\n\n const applySharedState = (transaction: Y.Transaction): void => {\n if (destroyed || transaction.origin === localOrigin) return;\n applyingRemote = true;\n try {\n const state = options.controller.getState();\n const nextSource = source.toString();\n let edit = new Transaction().withSource(\"collab:remote\").withMetadata({ addToHistory: false });\n if (nextSource !== state.markdown) edit = edit.replaceSource(0, state.markdown.length, nextSource);\n assertContract(doc);\n const nextSidecar = parseSidecar(sidecar.get(\"value\"), nextSource.length);\n if (JSON.stringify(nextSidecar) !== JSON.stringify(state.sidecar)) edit = edit.setSidecar(nextSidecar);\n if (edit.operations.length > 0) options.controller.dispatch(edit);\n } finally { applyingRemote = false; }\n };\n doc.on(\"afterTransaction\", applySharedState);\n\n const compactPending = (): Uint8Array => {\n const compactable = pendingUpdates.filter((entry) => entry !== inFlight);\n const update = Y.mergeUpdates(compactable.map((entry) => entry.update));\n if (update.byteLength > maxUpdateBytes) {\n throw new RangeError(`MDFN_COLLAB_UPDATE_TOO_LARGE:${maxUpdateBytes}`);\n }\n if (compactable.length === 0) return update;\n const replacement = { id: ++nextPendingId, update };\n pendingUpdates = inFlight ? [inFlight, replacement] : [replacement];\n emitAudit({ type: \"compact\", byteLength: update.byteLength, pending: pendingUpdates.length });\n return update;\n };\n\n const flush = async (): Promise<void> => {\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n if (!online || !options.sendUpdate || pendingUpdates.length === 0) return;\n if (flushing) return flushing;\n flushing = (async () => {\n while (online && pendingUpdates.length > 0) {\n const entry = pendingUpdates[0];\n inFlight = entry;\n try {\n await options.sendUpdate!(entry.update);\n pendingUpdates = pendingUpdates.filter((candidate) => candidate !== entry);\n emitAudit({ type: \"flush\", byteLength: entry.update.byteLength, pending: pendingUpdates.length });\n } finally {\n if (inFlight === entry) inFlight = null;\n }\n }\n })().finally(() => { flushing = null; });\n return flushing;\n };\n\n const queueLocalUpdate = (update: Uint8Array, origin: unknown): void => {\n if (destroyed || origin !== localOrigin) return;\n pendingUpdates.push({ id: ++nextPendingId, update: update.slice() });\n emitAudit({ type: \"local-update\", byteLength: update.byteLength, pending: pendingUpdates.length });\n const threshold = options.compactionThresholdBytes ?? 2 * 1024 * 1024;\n if (pendingUpdates.reduce((total, entry) => total + entry.update.byteLength, 0) > threshold) {\n try {\n compactPending();\n } catch (error) {\n if (!(error instanceof RangeError) || !error.message.startsWith(\"MDFN_COLLAB_UPDATE_TOO_LARGE:\")) throw error;\n }\n }\n if (online) void flush().catch((error: unknown) => {\n online = false;\n emitAudit({ type: \"offline\", pending: pendingUpdates.length, error: error instanceof Error ? error.message : String(error) });\n });\n };\n doc.on(\"update\", queueLocalUpdate);\n if (options.sendUpdate) {\n const initial = Y.encodeStateAsUpdate(doc);\n pendingUpdates.push({ id: ++nextPendingId, update: initial });\n emitAudit({ type: \"local-update\", byteLength: initial.byteLength, pending: pendingUpdates.length });\n if (online) void flush().catch((error: unknown) => {\n online = false;\n emitAudit({ type: \"offline\", pending: pendingUpdates.length, error: error instanceof Error ? error.message : String(error) });\n });\n }\n\n return {\n doc,\n awareness,\n encodeUpdate: (stateVector) => stateVector ? Y.encodeStateAsUpdate(doc, stateVector) : Y.encodeStateAsUpdate(doc),\n encodeStateVector: () => Y.encodeStateVector(doc),\n async applyUpdate(update, origin = \"transport\") {\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n const apply = async (): Promise<void> => {\n try {\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n if (update.byteLength > maxUpdateBytes) throw new RangeError(`MDFN_COLLAB_UPDATE_TOO_LARGE:${maxUpdateBytes}`);\n if (options.authorizeUpdate && !(await options.authorizeUpdate(update, origin))) throw new Error(\"MDFN_COLLAB_UPDATE_FORBIDDEN\");\n for (;;) {\n const baseline = await validateUpdate(update, origin);\n if (baseline !== documentGeneration) continue;\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n Y.applyUpdate(doc, update, origin);\n emitAudit({ type: \"remote-update\", byteLength: update.byteLength });\n return;\n }\n } catch (error) {\n emitAudit({ type: \"remote-rejected\", byteLength: update.byteLength, error: error instanceof Error ? error.message : String(error) });\n throw error;\n }\n };\n const queued = remoteApplication.then(apply, apply);\n remoteApplication = queued.catch(() => undefined);\n return queued;\n },\n setPresence(value) { if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\"); awareness.setLocalStateField(\"presence\", value); },\n getPresence: () => awareness.getStates(),\n isOnline: () => online,\n pendingUpdateCount: () => pendingUpdates.length,\n async setOnline(value) {\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n online = value;\n emitAudit({ type: value ? \"online\" : \"offline\", pending: pendingUpdates.length });\n if (value) await flush();\n },\n flush,\n compact: compactPending,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n unsubscribe();\n doc.off(\"afterTransaction\", applySharedState);\n doc.off(\"afterTransaction\", trackDocumentGeneration);\n doc.off(\"update\", queueLocalUpdate);\n pendingUpdates = [];\n awareness.setLocalState(null);\n if (ownsAwareness) awareness.destroy();\n },\n };\n}\n\nexport { Awareness, Y };\nexport const MDFN_COLLAB_VERSION = \"0.1.0\" as const;\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
export { Y };
|
|
3
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
4
|
+
export { Awareness } from 'y-protocols/awareness';
|
|
5
|
+
import { MdfnJsonValue, EditorController, MdfnSidecar } from '@mdfn/core';
|
|
6
|
+
|
|
7
|
+
interface CollaborationUser {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly name?: string;
|
|
10
|
+
readonly color?: string;
|
|
11
|
+
readonly metadata?: Readonly<Record<string, MdfnJsonValue>>;
|
|
12
|
+
}
|
|
13
|
+
interface CollaborationSessionOptions {
|
|
14
|
+
readonly controller: EditorController;
|
|
15
|
+
readonly documentId: string;
|
|
16
|
+
readonly user: CollaborationUser;
|
|
17
|
+
readonly doc?: Y.Doc;
|
|
18
|
+
readonly awareness?: Awareness;
|
|
19
|
+
readonly authorizeUpdate?: (update: Uint8Array, origin: unknown) => boolean | Promise<boolean>;
|
|
20
|
+
/** Protected editorial sidecar changes require an authoritative host decision. */
|
|
21
|
+
readonly authorizeSidecarUpdate?: (previous: MdfnSidecar | undefined, next: MdfnSidecar | undefined, origin: unknown) => boolean | Promise<boolean>;
|
|
22
|
+
readonly profileId?: string;
|
|
23
|
+
readonly protocolVersion?: number;
|
|
24
|
+
/** Maximum valid UTF-8 Markdown size; used to derive safe Yjs update limits. */
|
|
25
|
+
readonly maxDocumentBytes?: number;
|
|
26
|
+
readonly maxUpdateBytes?: number;
|
|
27
|
+
readonly online?: boolean;
|
|
28
|
+
readonly sendUpdate?: (update: Uint8Array) => void | Promise<void>;
|
|
29
|
+
readonly compactionThresholdBytes?: number;
|
|
30
|
+
readonly onAudit?: (event: CollaborationAuditEvent) => void;
|
|
31
|
+
}
|
|
32
|
+
interface CollaborationAuditEvent {
|
|
33
|
+
readonly type: "local-update" | "remote-update" | "remote-rejected" | "offline" | "online" | "flush" | "compact";
|
|
34
|
+
readonly documentId: string;
|
|
35
|
+
readonly userId: string;
|
|
36
|
+
readonly byteLength?: number;
|
|
37
|
+
readonly pending?: number;
|
|
38
|
+
readonly error?: string;
|
|
39
|
+
}
|
|
40
|
+
interface CollaborationSession {
|
|
41
|
+
readonly doc: Y.Doc;
|
|
42
|
+
readonly awareness: Awareness;
|
|
43
|
+
encodeUpdate(stateVector?: Uint8Array): Uint8Array;
|
|
44
|
+
encodeStateVector(): Uint8Array;
|
|
45
|
+
applyUpdate(update: Uint8Array, origin?: unknown): Promise<void>;
|
|
46
|
+
setPresence(value: Readonly<Record<string, MdfnJsonValue>>): void;
|
|
47
|
+
getPresence(): ReadonlyMap<number, unknown>;
|
|
48
|
+
isOnline(): boolean;
|
|
49
|
+
pendingUpdateCount(): number;
|
|
50
|
+
setOnline(online: boolean): Promise<void>;
|
|
51
|
+
flush(): Promise<void>;
|
|
52
|
+
compact(): Uint8Array;
|
|
53
|
+
destroy(): void;
|
|
54
|
+
}
|
|
55
|
+
declare function createCollaborationSession(options: CollaborationSessionOptions): CollaborationSession;
|
|
56
|
+
|
|
57
|
+
declare const MDFN_COLLAB_VERSION: "0.1.0";
|
|
58
|
+
|
|
59
|
+
export { type CollaborationAuditEvent, type CollaborationSession, type CollaborationSessionOptions, type CollaborationUser, MDFN_COLLAB_VERSION, createCollaborationSession };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
export { Y };
|
|
3
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
4
|
+
export { Awareness } from 'y-protocols/awareness';
|
|
5
|
+
import { MdfnJsonValue, EditorController, MdfnSidecar } from '@mdfn/core';
|
|
6
|
+
|
|
7
|
+
interface CollaborationUser {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly name?: string;
|
|
10
|
+
readonly color?: string;
|
|
11
|
+
readonly metadata?: Readonly<Record<string, MdfnJsonValue>>;
|
|
12
|
+
}
|
|
13
|
+
interface CollaborationSessionOptions {
|
|
14
|
+
readonly controller: EditorController;
|
|
15
|
+
readonly documentId: string;
|
|
16
|
+
readonly user: CollaborationUser;
|
|
17
|
+
readonly doc?: Y.Doc;
|
|
18
|
+
readonly awareness?: Awareness;
|
|
19
|
+
readonly authorizeUpdate?: (update: Uint8Array, origin: unknown) => boolean | Promise<boolean>;
|
|
20
|
+
/** Protected editorial sidecar changes require an authoritative host decision. */
|
|
21
|
+
readonly authorizeSidecarUpdate?: (previous: MdfnSidecar | undefined, next: MdfnSidecar | undefined, origin: unknown) => boolean | Promise<boolean>;
|
|
22
|
+
readonly profileId?: string;
|
|
23
|
+
readonly protocolVersion?: number;
|
|
24
|
+
/** Maximum valid UTF-8 Markdown size; used to derive safe Yjs update limits. */
|
|
25
|
+
readonly maxDocumentBytes?: number;
|
|
26
|
+
readonly maxUpdateBytes?: number;
|
|
27
|
+
readonly online?: boolean;
|
|
28
|
+
readonly sendUpdate?: (update: Uint8Array) => void | Promise<void>;
|
|
29
|
+
readonly compactionThresholdBytes?: number;
|
|
30
|
+
readonly onAudit?: (event: CollaborationAuditEvent) => void;
|
|
31
|
+
}
|
|
32
|
+
interface CollaborationAuditEvent {
|
|
33
|
+
readonly type: "local-update" | "remote-update" | "remote-rejected" | "offline" | "online" | "flush" | "compact";
|
|
34
|
+
readonly documentId: string;
|
|
35
|
+
readonly userId: string;
|
|
36
|
+
readonly byteLength?: number;
|
|
37
|
+
readonly pending?: number;
|
|
38
|
+
readonly error?: string;
|
|
39
|
+
}
|
|
40
|
+
interface CollaborationSession {
|
|
41
|
+
readonly doc: Y.Doc;
|
|
42
|
+
readonly awareness: Awareness;
|
|
43
|
+
encodeUpdate(stateVector?: Uint8Array): Uint8Array;
|
|
44
|
+
encodeStateVector(): Uint8Array;
|
|
45
|
+
applyUpdate(update: Uint8Array, origin?: unknown): Promise<void>;
|
|
46
|
+
setPresence(value: Readonly<Record<string, MdfnJsonValue>>): void;
|
|
47
|
+
getPresence(): ReadonlyMap<number, unknown>;
|
|
48
|
+
isOnline(): boolean;
|
|
49
|
+
pendingUpdateCount(): number;
|
|
50
|
+
setOnline(online: boolean): Promise<void>;
|
|
51
|
+
flush(): Promise<void>;
|
|
52
|
+
compact(): Uint8Array;
|
|
53
|
+
destroy(): void;
|
|
54
|
+
}
|
|
55
|
+
declare function createCollaborationSession(options: CollaborationSessionOptions): CollaborationSession;
|
|
56
|
+
|
|
57
|
+
declare const MDFN_COLLAB_VERSION: "0.1.0";
|
|
58
|
+
|
|
59
|
+
export { type CollaborationAuditEvent, type CollaborationSession, type CollaborationSessionOptions, type CollaborationUser, MDFN_COLLAB_VERSION, createCollaborationSession };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import { Awareness } from "y-protocols/awareness";
|
|
4
|
+
import { Transaction, validateMdfnSidecar } from "@mdfn/core";
|
|
5
|
+
function parseSidecar(value, markdownLength) {
|
|
6
|
+
if (value === void 0) return void 0;
|
|
7
|
+
try {
|
|
8
|
+
return validateMdfnSidecar(JSON.parse(value), { markdownLength });
|
|
9
|
+
} catch (error) {
|
|
10
|
+
throw new Error(`MDFN_COLLAB_SIDECAR_INVALID:${error instanceof Error ? error.message : String(error)}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function changedRange(previous, current) {
|
|
14
|
+
let from = 0;
|
|
15
|
+
while (from < previous.length && from < current.length && previous[from] === current[from]) from += 1;
|
|
16
|
+
let oldEnd = previous.length;
|
|
17
|
+
let newEnd = current.length;
|
|
18
|
+
while (oldEnd > from && newEnd > from && previous[oldEnd - 1] === current[newEnd - 1]) {
|
|
19
|
+
oldEnd -= 1;
|
|
20
|
+
newEnd -= 1;
|
|
21
|
+
}
|
|
22
|
+
return { from, to: oldEnd, inserted: current.slice(from, newEnd) };
|
|
23
|
+
}
|
|
24
|
+
function createCollaborationSession(options) {
|
|
25
|
+
const doc = options.doc ?? new Y.Doc({ guid: options.documentId });
|
|
26
|
+
const ownsAwareness = !options.awareness;
|
|
27
|
+
const awareness = options.awareness ?? new Awareness(doc);
|
|
28
|
+
const source = doc.getText("markdown");
|
|
29
|
+
const sidecar = doc.getMap("sidecar");
|
|
30
|
+
const metadata = doc.getMap("metadata");
|
|
31
|
+
const sharedDocumentInitialized = options.doc !== void 0 && (source.length > 0 || sidecar.size > 0 || metadata.size > 0);
|
|
32
|
+
const localOrigin = Object.freeze({ session: options.user.id });
|
|
33
|
+
let applyingRemote = false;
|
|
34
|
+
let destroyed = false;
|
|
35
|
+
let online = options.online ?? true;
|
|
36
|
+
let nextPendingId = 0;
|
|
37
|
+
let pendingUpdates = [];
|
|
38
|
+
let inFlight = null;
|
|
39
|
+
let flushing = null;
|
|
40
|
+
let remoteApplication = Promise.resolve();
|
|
41
|
+
let documentGeneration = 0;
|
|
42
|
+
const maxDocumentBytes = options.maxDocumentBytes ?? 2 * 1024 * 1024;
|
|
43
|
+
const maxUpdateBytes = options.maxUpdateBytes ?? maxDocumentBytes + Math.max(64 * 1024, Math.ceil(maxDocumentBytes / 16));
|
|
44
|
+
const emitAudit = (event) => {
|
|
45
|
+
options.onAudit?.({ ...event, documentId: options.documentId, userId: options.user.id });
|
|
46
|
+
};
|
|
47
|
+
const expectedSchema = options.controller.getState().schemaHash;
|
|
48
|
+
const expectedDocumentId = options.documentId;
|
|
49
|
+
const expectedProfile = options.profileId ?? "default";
|
|
50
|
+
const expectedProtocol = String(options.protocolVersion ?? 1);
|
|
51
|
+
const expectedExtensions = JSON.stringify(options.controller.extensions.extensions.map((extension) => ({ name: extension.name, version: extension.version })));
|
|
52
|
+
const bindMetadata = (key, expected, code) => {
|
|
53
|
+
const current = metadata.get(key);
|
|
54
|
+
if (current !== void 0 && current !== expected) throw new Error(`${code}:${current}:${expected}`);
|
|
55
|
+
if (current === void 0) metadata.set(key, expected);
|
|
56
|
+
};
|
|
57
|
+
bindMetadata("schemaHash", expectedSchema, "MDFN_COLLAB_SCHEMA_MISMATCH");
|
|
58
|
+
bindMetadata("documentId", expectedDocumentId, "MDFN_COLLAB_DOCUMENT_MISMATCH");
|
|
59
|
+
bindMetadata("profileId", expectedProfile, "MDFN_COLLAB_PROFILE_MISMATCH");
|
|
60
|
+
bindMetadata("protocolVersion", expectedProtocol, "MDFN_COLLAB_PROTOCOL_MISMATCH");
|
|
61
|
+
bindMetadata("extensions", expectedExtensions, "MDFN_COLLAB_EXTENSIONS_MISMATCH");
|
|
62
|
+
const assertContract = (candidate) => {
|
|
63
|
+
const candidateMetadata = candidate.getMap("metadata");
|
|
64
|
+
const schema = candidateMetadata.get("schemaHash");
|
|
65
|
+
if (schema !== expectedSchema) throw new Error(`MDFN_COLLAB_SCHEMA_MISMATCH:${schema ?? "missing"}:${expectedSchema}`);
|
|
66
|
+
if (candidateMetadata.get("documentId") !== expectedDocumentId) throw new Error("MDFN_COLLAB_DOCUMENT_MISMATCH");
|
|
67
|
+
if (candidateMetadata.get("profileId") !== expectedProfile) throw new Error("MDFN_COLLAB_PROFILE_MISMATCH");
|
|
68
|
+
if (candidateMetadata.get("protocolVersion") !== expectedProtocol) throw new Error("MDFN_COLLAB_PROTOCOL_MISMATCH");
|
|
69
|
+
if (candidateMetadata.get("extensions") !== expectedExtensions) throw new Error("MDFN_COLLAB_EXTENSIONS_MISMATCH");
|
|
70
|
+
const markdown = candidate.getText("markdown").toString();
|
|
71
|
+
try {
|
|
72
|
+
options.controller.validateMarkdown(markdown);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new Error(`MDFN_COLLAB_MARKDOWN_INVALID:${error instanceof Error ? error.message : String(error)}`);
|
|
75
|
+
}
|
|
76
|
+
parseSidecar(candidate.getMap("sidecar").get("value"), markdown.length);
|
|
77
|
+
};
|
|
78
|
+
const canonicalJson = (value) => {
|
|
79
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
80
|
+
if (value === null || typeof value !== "object") return value;
|
|
81
|
+
return Object.fromEntries(
|
|
82
|
+
Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, canonicalJson(entry)])
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
const protectedSidecar = (value) => JSON.stringify(canonicalJson({
|
|
86
|
+
comments: value?.comments ?? [],
|
|
87
|
+
suggestions: value?.suggestions ?? [],
|
|
88
|
+
reviewState: value?.reviewState ?? "draft",
|
|
89
|
+
audit: value?.audit ?? []
|
|
90
|
+
}));
|
|
91
|
+
const validateUpdate = async (update, origin) => {
|
|
92
|
+
const baseline = documentGeneration;
|
|
93
|
+
const candidate = new Y.Doc({ guid: expectedDocumentId });
|
|
94
|
+
try {
|
|
95
|
+
Y.applyUpdate(candidate, Y.encodeStateAsUpdate(doc), "validation:baseline");
|
|
96
|
+
Y.applyUpdate(candidate, update, "validation:candidate");
|
|
97
|
+
assertContract(candidate);
|
|
98
|
+
const previousSidecar = parseSidecar(sidecar.get("value"), source.length);
|
|
99
|
+
const candidateSidecar = parseSidecar(candidate.getMap("sidecar").get("value"), candidate.getText("markdown").length);
|
|
100
|
+
if (protectedSidecar(previousSidecar) !== protectedSidecar(candidateSidecar)) {
|
|
101
|
+
if (!options.authorizeSidecarUpdate || !await options.authorizeSidecarUpdate(previousSidecar, candidateSidecar, origin)) {
|
|
102
|
+
throw new Error("MDFN_COLLAB_EDITORIAL_UPDATE_FORBIDDEN");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return baseline;
|
|
106
|
+
} finally {
|
|
107
|
+
candidate.destroy();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
if (!sharedDocumentInitialized) {
|
|
111
|
+
if (source.length === 0) source.insert(0, options.controller.getState().markdown);
|
|
112
|
+
if (!sidecar.has("value") && options.controller.getState().sidecar) sidecar.set("value", JSON.stringify(options.controller.getState().sidecar));
|
|
113
|
+
} else {
|
|
114
|
+
assertContract(doc);
|
|
115
|
+
const state = options.controller.getState();
|
|
116
|
+
const sharedMarkdown = source.toString();
|
|
117
|
+
const sharedSidecar = parseSidecar(sidecar.get("value"), sharedMarkdown.length);
|
|
118
|
+
let initial = new Transaction().withSource("collab:initial").withMetadata({ addToHistory: false });
|
|
119
|
+
if (sharedMarkdown !== state.markdown) initial = initial.replaceSource(0, state.markdown.length, sharedMarkdown);
|
|
120
|
+
if (JSON.stringify(sharedSidecar) !== JSON.stringify(state.sidecar)) initial = initial.setSidecar(sharedSidecar);
|
|
121
|
+
if (initial.operations.length > 0) options.controller.dispatch(initial);
|
|
122
|
+
}
|
|
123
|
+
awareness.setLocalState({ user: options.user, documentId: options.documentId });
|
|
124
|
+
const trackDocumentGeneration = () => {
|
|
125
|
+
documentGeneration += 1;
|
|
126
|
+
};
|
|
127
|
+
doc.on("afterTransaction", trackDocumentGeneration);
|
|
128
|
+
const unsubscribe = options.controller.subscribe((change) => {
|
|
129
|
+
if (applyingRemote || destroyed) return;
|
|
130
|
+
doc.transact(() => {
|
|
131
|
+
if (change.documentChanged) {
|
|
132
|
+
const current = source.toString();
|
|
133
|
+
const range = changedRange(current, change.current.markdown);
|
|
134
|
+
if (range.to > range.from) source.delete(range.from, range.to - range.from);
|
|
135
|
+
if (range.inserted) source.insert(range.from, range.inserted);
|
|
136
|
+
}
|
|
137
|
+
if (change.sidecarChanged) {
|
|
138
|
+
if (change.current.sidecar) sidecar.set("value", JSON.stringify(change.current.sidecar));
|
|
139
|
+
else sidecar.delete("value");
|
|
140
|
+
}
|
|
141
|
+
}, localOrigin);
|
|
142
|
+
});
|
|
143
|
+
const applySharedState = (transaction) => {
|
|
144
|
+
if (destroyed || transaction.origin === localOrigin) return;
|
|
145
|
+
applyingRemote = true;
|
|
146
|
+
try {
|
|
147
|
+
const state = options.controller.getState();
|
|
148
|
+
const nextSource = source.toString();
|
|
149
|
+
let edit = new Transaction().withSource("collab:remote").withMetadata({ addToHistory: false });
|
|
150
|
+
if (nextSource !== state.markdown) edit = edit.replaceSource(0, state.markdown.length, nextSource);
|
|
151
|
+
assertContract(doc);
|
|
152
|
+
const nextSidecar = parseSidecar(sidecar.get("value"), nextSource.length);
|
|
153
|
+
if (JSON.stringify(nextSidecar) !== JSON.stringify(state.sidecar)) edit = edit.setSidecar(nextSidecar);
|
|
154
|
+
if (edit.operations.length > 0) options.controller.dispatch(edit);
|
|
155
|
+
} finally {
|
|
156
|
+
applyingRemote = false;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
doc.on("afterTransaction", applySharedState);
|
|
160
|
+
const compactPending = () => {
|
|
161
|
+
const compactable = pendingUpdates.filter((entry) => entry !== inFlight);
|
|
162
|
+
const update = Y.mergeUpdates(compactable.map((entry) => entry.update));
|
|
163
|
+
if (update.byteLength > maxUpdateBytes) {
|
|
164
|
+
throw new RangeError(`MDFN_COLLAB_UPDATE_TOO_LARGE:${maxUpdateBytes}`);
|
|
165
|
+
}
|
|
166
|
+
if (compactable.length === 0) return update;
|
|
167
|
+
const replacement = { id: ++nextPendingId, update };
|
|
168
|
+
pendingUpdates = inFlight ? [inFlight, replacement] : [replacement];
|
|
169
|
+
emitAudit({ type: "compact", byteLength: update.byteLength, pending: pendingUpdates.length });
|
|
170
|
+
return update;
|
|
171
|
+
};
|
|
172
|
+
const flush = async () => {
|
|
173
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
174
|
+
if (!online || !options.sendUpdate || pendingUpdates.length === 0) return;
|
|
175
|
+
if (flushing) return flushing;
|
|
176
|
+
flushing = (async () => {
|
|
177
|
+
while (online && pendingUpdates.length > 0) {
|
|
178
|
+
const entry = pendingUpdates[0];
|
|
179
|
+
inFlight = entry;
|
|
180
|
+
try {
|
|
181
|
+
await options.sendUpdate(entry.update);
|
|
182
|
+
pendingUpdates = pendingUpdates.filter((candidate) => candidate !== entry);
|
|
183
|
+
emitAudit({ type: "flush", byteLength: entry.update.byteLength, pending: pendingUpdates.length });
|
|
184
|
+
} finally {
|
|
185
|
+
if (inFlight === entry) inFlight = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
})().finally(() => {
|
|
189
|
+
flushing = null;
|
|
190
|
+
});
|
|
191
|
+
return flushing;
|
|
192
|
+
};
|
|
193
|
+
const queueLocalUpdate = (update, origin) => {
|
|
194
|
+
if (destroyed || origin !== localOrigin) return;
|
|
195
|
+
pendingUpdates.push({ id: ++nextPendingId, update: update.slice() });
|
|
196
|
+
emitAudit({ type: "local-update", byteLength: update.byteLength, pending: pendingUpdates.length });
|
|
197
|
+
const threshold = options.compactionThresholdBytes ?? 2 * 1024 * 1024;
|
|
198
|
+
if (pendingUpdates.reduce((total, entry) => total + entry.update.byteLength, 0) > threshold) {
|
|
199
|
+
try {
|
|
200
|
+
compactPending();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (!(error instanceof RangeError) || !error.message.startsWith("MDFN_COLLAB_UPDATE_TOO_LARGE:")) throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (online) void flush().catch((error) => {
|
|
206
|
+
online = false;
|
|
207
|
+
emitAudit({ type: "offline", pending: pendingUpdates.length, error: error instanceof Error ? error.message : String(error) });
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
doc.on("update", queueLocalUpdate);
|
|
211
|
+
if (options.sendUpdate) {
|
|
212
|
+
const initial = Y.encodeStateAsUpdate(doc);
|
|
213
|
+
pendingUpdates.push({ id: ++nextPendingId, update: initial });
|
|
214
|
+
emitAudit({ type: "local-update", byteLength: initial.byteLength, pending: pendingUpdates.length });
|
|
215
|
+
if (online) void flush().catch((error) => {
|
|
216
|
+
online = false;
|
|
217
|
+
emitAudit({ type: "offline", pending: pendingUpdates.length, error: error instanceof Error ? error.message : String(error) });
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
doc,
|
|
222
|
+
awareness,
|
|
223
|
+
encodeUpdate: (stateVector) => stateVector ? Y.encodeStateAsUpdate(doc, stateVector) : Y.encodeStateAsUpdate(doc),
|
|
224
|
+
encodeStateVector: () => Y.encodeStateVector(doc),
|
|
225
|
+
async applyUpdate(update, origin = "transport") {
|
|
226
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
227
|
+
const apply = async () => {
|
|
228
|
+
try {
|
|
229
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
230
|
+
if (update.byteLength > maxUpdateBytes) throw new RangeError(`MDFN_COLLAB_UPDATE_TOO_LARGE:${maxUpdateBytes}`);
|
|
231
|
+
if (options.authorizeUpdate && !await options.authorizeUpdate(update, origin)) throw new Error("MDFN_COLLAB_UPDATE_FORBIDDEN");
|
|
232
|
+
for (; ; ) {
|
|
233
|
+
const baseline = await validateUpdate(update, origin);
|
|
234
|
+
if (baseline !== documentGeneration) continue;
|
|
235
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
236
|
+
Y.applyUpdate(doc, update, origin);
|
|
237
|
+
emitAudit({ type: "remote-update", byteLength: update.byteLength });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
emitAudit({ type: "remote-rejected", byteLength: update.byteLength, error: error instanceof Error ? error.message : String(error) });
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const queued = remoteApplication.then(apply, apply);
|
|
246
|
+
remoteApplication = queued.catch(() => void 0);
|
|
247
|
+
return queued;
|
|
248
|
+
},
|
|
249
|
+
setPresence(value) {
|
|
250
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
251
|
+
awareness.setLocalStateField("presence", value);
|
|
252
|
+
},
|
|
253
|
+
getPresence: () => awareness.getStates(),
|
|
254
|
+
isOnline: () => online,
|
|
255
|
+
pendingUpdateCount: () => pendingUpdates.length,
|
|
256
|
+
async setOnline(value) {
|
|
257
|
+
if (destroyed) throw new Error("MDFN_COLLAB_DESTROYED");
|
|
258
|
+
online = value;
|
|
259
|
+
emitAudit({ type: value ? "online" : "offline", pending: pendingUpdates.length });
|
|
260
|
+
if (value) await flush();
|
|
261
|
+
},
|
|
262
|
+
flush,
|
|
263
|
+
compact: compactPending,
|
|
264
|
+
destroy() {
|
|
265
|
+
if (destroyed) return;
|
|
266
|
+
destroyed = true;
|
|
267
|
+
unsubscribe();
|
|
268
|
+
doc.off("afterTransaction", applySharedState);
|
|
269
|
+
doc.off("afterTransaction", trackDocumentGeneration);
|
|
270
|
+
doc.off("update", queueLocalUpdate);
|
|
271
|
+
pendingUpdates = [];
|
|
272
|
+
awareness.setLocalState(null);
|
|
273
|
+
if (ownsAwareness) awareness.destroy();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
var MDFN_COLLAB_VERSION = "0.1.0";
|
|
278
|
+
export {
|
|
279
|
+
Awareness,
|
|
280
|
+
MDFN_COLLAB_VERSION,
|
|
281
|
+
Y,
|
|
282
|
+
createCollaborationSession
|
|
283
|
+
};
|
|
284
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import * as Y from \"yjs\";\nimport { Awareness } from \"y-protocols/awareness\";\nimport { Transaction, validateMdfnSidecar, type EditorController, type MdfnJsonValue, type MdfnSidecar } from \"@mdfn/core\";\n\nexport interface CollaborationUser {\n readonly id: string;\n readonly name?: string;\n readonly color?: string;\n readonly metadata?: Readonly<Record<string, MdfnJsonValue>>;\n}\n\nexport interface CollaborationSessionOptions {\n readonly controller: EditorController;\n readonly documentId: string;\n readonly user: CollaborationUser;\n readonly doc?: Y.Doc;\n readonly awareness?: Awareness;\n readonly authorizeUpdate?: (update: Uint8Array, origin: unknown) => boolean | Promise<boolean>;\n /** Protected editorial sidecar changes require an authoritative host decision. */\n readonly authorizeSidecarUpdate?: (previous: MdfnSidecar | undefined, next: MdfnSidecar | undefined, origin: unknown) => boolean | Promise<boolean>;\n readonly profileId?: string;\n readonly protocolVersion?: number;\n /** Maximum valid UTF-8 Markdown size; used to derive safe Yjs update limits. */\n readonly maxDocumentBytes?: number;\n readonly maxUpdateBytes?: number;\n readonly online?: boolean;\n readonly sendUpdate?: (update: Uint8Array) => void | Promise<void>;\n readonly compactionThresholdBytes?: number;\n readonly onAudit?: (event: CollaborationAuditEvent) => void;\n}\n\nexport interface CollaborationAuditEvent {\n readonly type: \"local-update\" | \"remote-update\" | \"remote-rejected\" | \"offline\" | \"online\" | \"flush\" | \"compact\";\n readonly documentId: string;\n readonly userId: string;\n readonly byteLength?: number;\n readonly pending?: number;\n readonly error?: string;\n}\n\nfunction parseSidecar(value: string | undefined, markdownLength?: number): MdfnSidecar | undefined {\n if (value === undefined) return undefined;\n try {\n return validateMdfnSidecar(JSON.parse(value), { markdownLength });\n } catch (error) {\n throw new Error(`MDFN_COLLAB_SIDECAR_INVALID:${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\nexport interface CollaborationSession {\n readonly doc: Y.Doc;\n readonly awareness: Awareness;\n encodeUpdate(stateVector?: Uint8Array): Uint8Array;\n encodeStateVector(): Uint8Array;\n applyUpdate(update: Uint8Array, origin?: unknown): Promise<void>;\n setPresence(value: Readonly<Record<string, MdfnJsonValue>>): void;\n getPresence(): ReadonlyMap<number, unknown>;\n isOnline(): boolean;\n pendingUpdateCount(): number;\n setOnline(online: boolean): Promise<void>;\n flush(): Promise<void>;\n compact(): Uint8Array;\n destroy(): void;\n}\n\nfunction changedRange(previous: string, current: string): { from: number; to: number; inserted: string } {\n let from = 0;\n while (from < previous.length && from < current.length && previous[from] === current[from]) from += 1;\n let oldEnd = previous.length;\n let newEnd = current.length;\n while (oldEnd > from && newEnd > from && previous[oldEnd - 1] === current[newEnd - 1]) { oldEnd -= 1; newEnd -= 1; }\n return { from, to: oldEnd, inserted: current.slice(from, newEnd) };\n}\n\nexport function createCollaborationSession(options: CollaborationSessionOptions): CollaborationSession {\n const doc = options.doc ?? new Y.Doc({ guid: options.documentId });\n const ownsAwareness = !options.awareness;\n const awareness = options.awareness ?? new Awareness(doc);\n const source = doc.getText(\"markdown\");\n const sidecar = doc.getMap<string>(\"sidecar\");\n const metadata = doc.getMap<string>(\"metadata\");\n const sharedDocumentInitialized = options.doc !== undefined && (source.length > 0 || sidecar.size > 0 || metadata.size > 0);\n const localOrigin = Object.freeze({ session: options.user.id });\n let applyingRemote = false;\n let destroyed = false;\n let online = options.online ?? true;\n interface PendingUpdate { readonly id: number; readonly update: Uint8Array; }\n let nextPendingId = 0;\n let pendingUpdates: PendingUpdate[] = [];\n let inFlight: PendingUpdate | null = null;\n let flushing: Promise<void> | null = null;\n let remoteApplication: Promise<void> = Promise.resolve();\n let documentGeneration = 0;\n const maxDocumentBytes = options.maxDocumentBytes ?? 2 * 1024 * 1024;\n const maxUpdateBytes = options.maxUpdateBytes ?? maxDocumentBytes + Math.max(64 * 1024, Math.ceil(maxDocumentBytes / 16));\n\n const emitAudit = (event: Omit<CollaborationAuditEvent, \"documentId\" | \"userId\">): void => {\n options.onAudit?.({ ...event, documentId: options.documentId, userId: options.user.id });\n };\n\n const expectedSchema = options.controller.getState().schemaHash;\n const expectedDocumentId = options.documentId;\n const expectedProfile = options.profileId ?? \"default\";\n const expectedProtocol = String(options.protocolVersion ?? 1);\n const expectedExtensions = JSON.stringify(options.controller.extensions.extensions.map((extension) => ({ name: extension.name, version: extension.version })));\n const bindMetadata = (key: string, expected: string, code: string): void => {\n const current = metadata.get(key);\n if (current !== undefined && current !== expected) throw new Error(`${code}:${current}:${expected}`);\n if (current === undefined) metadata.set(key, expected);\n };\n bindMetadata(\"schemaHash\", expectedSchema, \"MDFN_COLLAB_SCHEMA_MISMATCH\");\n bindMetadata(\"documentId\", expectedDocumentId, \"MDFN_COLLAB_DOCUMENT_MISMATCH\");\n bindMetadata(\"profileId\", expectedProfile, \"MDFN_COLLAB_PROFILE_MISMATCH\");\n bindMetadata(\"protocolVersion\", expectedProtocol, \"MDFN_COLLAB_PROTOCOL_MISMATCH\");\n bindMetadata(\"extensions\", expectedExtensions, \"MDFN_COLLAB_EXTENSIONS_MISMATCH\");\n\n const assertContract = (candidate: Y.Doc): void => {\n const candidateMetadata = candidate.getMap<string>(\"metadata\");\n const schema = candidateMetadata.get(\"schemaHash\");\n if (schema !== expectedSchema) throw new Error(`MDFN_COLLAB_SCHEMA_MISMATCH:${schema ?? \"missing\"}:${expectedSchema}`);\n if (candidateMetadata.get(\"documentId\") !== expectedDocumentId) throw new Error(\"MDFN_COLLAB_DOCUMENT_MISMATCH\");\n if (candidateMetadata.get(\"profileId\") !== expectedProfile) throw new Error(\"MDFN_COLLAB_PROFILE_MISMATCH\");\n if (candidateMetadata.get(\"protocolVersion\") !== expectedProtocol) throw new Error(\"MDFN_COLLAB_PROTOCOL_MISMATCH\");\n if (candidateMetadata.get(\"extensions\") !== expectedExtensions) throw new Error(\"MDFN_COLLAB_EXTENSIONS_MISMATCH\");\n const markdown = candidate.getText(\"markdown\").toString();\n try {\n options.controller.validateMarkdown(markdown);\n } catch (error) {\n throw new Error(`MDFN_COLLAB_MARKDOWN_INVALID:${error instanceof Error ? error.message : String(error)}`);\n }\n parseSidecar(candidate.getMap<string>(\"sidecar\").get(\"value\"), markdown.length);\n };\n\n const canonicalJson = (value: unknown): unknown => {\n if (Array.isArray(value)) return value.map(canonicalJson);\n if (value === null || typeof value !== \"object\") return value;\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)\n .map(([key, entry]) => [key, canonicalJson(entry)]),\n );\n };\n const protectedSidecar = (value: MdfnSidecar | undefined): string => JSON.stringify(canonicalJson({\n comments: value?.comments ?? [],\n suggestions: value?.suggestions ?? [],\n reviewState: value?.reviewState ?? \"draft\",\n audit: value?.audit ?? [],\n }));\n\n const validateUpdate = async (update: Uint8Array, origin: unknown): Promise<number> => {\n const baseline = documentGeneration;\n const candidate = new Y.Doc({ guid: expectedDocumentId });\n try {\n Y.applyUpdate(candidate, Y.encodeStateAsUpdate(doc), \"validation:baseline\");\n Y.applyUpdate(candidate, update, \"validation:candidate\");\n assertContract(candidate);\n const previousSidecar = parseSidecar(sidecar.get(\"value\"), source.length);\n const candidateSidecar = parseSidecar(candidate.getMap<string>(\"sidecar\").get(\"value\"), candidate.getText(\"markdown\").length);\n if (protectedSidecar(previousSidecar) !== protectedSidecar(candidateSidecar)) {\n if (!options.authorizeSidecarUpdate || !(await options.authorizeSidecarUpdate(previousSidecar, candidateSidecar, origin))) {\n throw new Error(\"MDFN_COLLAB_EDITORIAL_UPDATE_FORBIDDEN\");\n }\n }\n return baseline;\n } finally {\n candidate.destroy();\n }\n };\n\n if (!sharedDocumentInitialized) {\n if (source.length === 0) source.insert(0, options.controller.getState().markdown);\n if (!sidecar.has(\"value\") && options.controller.getState().sidecar) sidecar.set(\"value\", JSON.stringify(options.controller.getState().sidecar));\n } else {\n assertContract(doc);\n const state = options.controller.getState();\n const sharedMarkdown = source.toString();\n const sharedSidecar = parseSidecar(sidecar.get(\"value\"), sharedMarkdown.length);\n let initial = new Transaction().withSource(\"collab:initial\").withMetadata({ addToHistory: false });\n if (sharedMarkdown !== state.markdown) initial = initial.replaceSource(0, state.markdown.length, sharedMarkdown);\n if (JSON.stringify(sharedSidecar) !== JSON.stringify(state.sidecar)) initial = initial.setSidecar(sharedSidecar);\n if (initial.operations.length > 0) options.controller.dispatch(initial);\n }\n\n awareness.setLocalState({ user: options.user, documentId: options.documentId });\n\n const trackDocumentGeneration = (): void => { documentGeneration += 1; };\n doc.on(\"afterTransaction\", trackDocumentGeneration);\n\n const unsubscribe = options.controller.subscribe((change) => {\n if (applyingRemote || destroyed) return;\n doc.transact(() => {\n if (change.documentChanged) {\n const current = source.toString();\n const range = changedRange(current, change.current.markdown);\n if (range.to > range.from) source.delete(range.from, range.to - range.from);\n if (range.inserted) source.insert(range.from, range.inserted);\n }\n if (change.sidecarChanged) {\n if (change.current.sidecar) sidecar.set(\"value\", JSON.stringify(change.current.sidecar));\n else sidecar.delete(\"value\");\n }\n }, localOrigin);\n });\n\n const applySharedState = (transaction: Y.Transaction): void => {\n if (destroyed || transaction.origin === localOrigin) return;\n applyingRemote = true;\n try {\n const state = options.controller.getState();\n const nextSource = source.toString();\n let edit = new Transaction().withSource(\"collab:remote\").withMetadata({ addToHistory: false });\n if (nextSource !== state.markdown) edit = edit.replaceSource(0, state.markdown.length, nextSource);\n assertContract(doc);\n const nextSidecar = parseSidecar(sidecar.get(\"value\"), nextSource.length);\n if (JSON.stringify(nextSidecar) !== JSON.stringify(state.sidecar)) edit = edit.setSidecar(nextSidecar);\n if (edit.operations.length > 0) options.controller.dispatch(edit);\n } finally { applyingRemote = false; }\n };\n doc.on(\"afterTransaction\", applySharedState);\n\n const compactPending = (): Uint8Array => {\n const compactable = pendingUpdates.filter((entry) => entry !== inFlight);\n const update = Y.mergeUpdates(compactable.map((entry) => entry.update));\n if (update.byteLength > maxUpdateBytes) {\n throw new RangeError(`MDFN_COLLAB_UPDATE_TOO_LARGE:${maxUpdateBytes}`);\n }\n if (compactable.length === 0) return update;\n const replacement = { id: ++nextPendingId, update };\n pendingUpdates = inFlight ? [inFlight, replacement] : [replacement];\n emitAudit({ type: \"compact\", byteLength: update.byteLength, pending: pendingUpdates.length });\n return update;\n };\n\n const flush = async (): Promise<void> => {\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n if (!online || !options.sendUpdate || pendingUpdates.length === 0) return;\n if (flushing) return flushing;\n flushing = (async () => {\n while (online && pendingUpdates.length > 0) {\n const entry = pendingUpdates[0];\n inFlight = entry;\n try {\n await options.sendUpdate!(entry.update);\n pendingUpdates = pendingUpdates.filter((candidate) => candidate !== entry);\n emitAudit({ type: \"flush\", byteLength: entry.update.byteLength, pending: pendingUpdates.length });\n } finally {\n if (inFlight === entry) inFlight = null;\n }\n }\n })().finally(() => { flushing = null; });\n return flushing;\n };\n\n const queueLocalUpdate = (update: Uint8Array, origin: unknown): void => {\n if (destroyed || origin !== localOrigin) return;\n pendingUpdates.push({ id: ++nextPendingId, update: update.slice() });\n emitAudit({ type: \"local-update\", byteLength: update.byteLength, pending: pendingUpdates.length });\n const threshold = options.compactionThresholdBytes ?? 2 * 1024 * 1024;\n if (pendingUpdates.reduce((total, entry) => total + entry.update.byteLength, 0) > threshold) {\n try {\n compactPending();\n } catch (error) {\n if (!(error instanceof RangeError) || !error.message.startsWith(\"MDFN_COLLAB_UPDATE_TOO_LARGE:\")) throw error;\n }\n }\n if (online) void flush().catch((error: unknown) => {\n online = false;\n emitAudit({ type: \"offline\", pending: pendingUpdates.length, error: error instanceof Error ? error.message : String(error) });\n });\n };\n doc.on(\"update\", queueLocalUpdate);\n if (options.sendUpdate) {\n const initial = Y.encodeStateAsUpdate(doc);\n pendingUpdates.push({ id: ++nextPendingId, update: initial });\n emitAudit({ type: \"local-update\", byteLength: initial.byteLength, pending: pendingUpdates.length });\n if (online) void flush().catch((error: unknown) => {\n online = false;\n emitAudit({ type: \"offline\", pending: pendingUpdates.length, error: error instanceof Error ? error.message : String(error) });\n });\n }\n\n return {\n doc,\n awareness,\n encodeUpdate: (stateVector) => stateVector ? Y.encodeStateAsUpdate(doc, stateVector) : Y.encodeStateAsUpdate(doc),\n encodeStateVector: () => Y.encodeStateVector(doc),\n async applyUpdate(update, origin = \"transport\") {\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n const apply = async (): Promise<void> => {\n try {\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n if (update.byteLength > maxUpdateBytes) throw new RangeError(`MDFN_COLLAB_UPDATE_TOO_LARGE:${maxUpdateBytes}`);\n if (options.authorizeUpdate && !(await options.authorizeUpdate(update, origin))) throw new Error(\"MDFN_COLLAB_UPDATE_FORBIDDEN\");\n for (;;) {\n const baseline = await validateUpdate(update, origin);\n if (baseline !== documentGeneration) continue;\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n Y.applyUpdate(doc, update, origin);\n emitAudit({ type: \"remote-update\", byteLength: update.byteLength });\n return;\n }\n } catch (error) {\n emitAudit({ type: \"remote-rejected\", byteLength: update.byteLength, error: error instanceof Error ? error.message : String(error) });\n throw error;\n }\n };\n const queued = remoteApplication.then(apply, apply);\n remoteApplication = queued.catch(() => undefined);\n return queued;\n },\n setPresence(value) { if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\"); awareness.setLocalStateField(\"presence\", value); },\n getPresence: () => awareness.getStates(),\n isOnline: () => online,\n pendingUpdateCount: () => pendingUpdates.length,\n async setOnline(value) {\n if (destroyed) throw new Error(\"MDFN_COLLAB_DESTROYED\");\n online = value;\n emitAudit({ type: value ? \"online\" : \"offline\", pending: pendingUpdates.length });\n if (value) await flush();\n },\n flush,\n compact: compactPending,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n unsubscribe();\n doc.off(\"afterTransaction\", applySharedState);\n doc.off(\"afterTransaction\", trackDocumentGeneration);\n doc.off(\"update\", queueLocalUpdate);\n pendingUpdates = [];\n awareness.setLocalState(null);\n if (ownsAwareness) awareness.destroy();\n },\n };\n}\n\nexport { Awareness, Y };\nexport const MDFN_COLLAB_VERSION = \"0.1.0\" as const;\n"],"mappings":";AAAA,YAAY,OAAO;AACnB,SAAS,iBAAiB;AAC1B,SAAS,aAAa,2BAAwF;AAsC9G,SAAS,aAAa,OAA2B,gBAAkD;AACjG,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI;AACF,WAAO,oBAAoB,KAAK,MAAM,KAAK,GAAG,EAAE,eAAe,CAAC;AAAA,EAClE,SAAS,OAAO;AACd,UAAM,IAAI,MAAM,+BAA+B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,EAAE;AAAA,EACzG;AACF;AAkBA,SAAS,aAAa,UAAkB,SAAiE;AACvG,MAAI,OAAO;AACX,SAAO,OAAO,SAAS,UAAU,OAAO,QAAQ,UAAU,SAAS,IAAI,MAAM,QAAQ,IAAI,EAAG,SAAQ;AACpG,MAAI,SAAS,SAAS;AACtB,MAAI,SAAS,QAAQ;AACrB,SAAO,SAAS,QAAQ,SAAS,QAAQ,SAAS,SAAS,CAAC,MAAM,QAAQ,SAAS,CAAC,GAAG;AAAE,cAAU;AAAG,cAAU;AAAA,EAAG;AACnH,SAAO,EAAE,MAAM,IAAI,QAAQ,UAAU,QAAQ,MAAM,MAAM,MAAM,EAAE;AACnE;AAEO,SAAS,2BAA2B,SAA4D;AACrG,QAAM,MAAM,QAAQ,OAAO,IAAM,MAAI,EAAE,MAAM,QAAQ,WAAW,CAAC;AACjE,QAAM,gBAAgB,CAAC,QAAQ;AAC/B,QAAM,YAAY,QAAQ,aAAa,IAAI,UAAU,GAAG;AACxD,QAAM,SAAS,IAAI,QAAQ,UAAU;AACrC,QAAM,UAAU,IAAI,OAAe,SAAS;AAC5C,QAAM,WAAW,IAAI,OAAe,UAAU;AAC9C,QAAM,4BAA4B,QAAQ,QAAQ,WAAc,OAAO,SAAS,KAAK,QAAQ,OAAO,KAAK,SAAS,OAAO;AACzH,QAAM,cAAc,OAAO,OAAO,EAAE,SAAS,QAAQ,KAAK,GAAG,CAAC;AAC9D,MAAI,iBAAiB;AACrB,MAAI,YAAY;AAChB,MAAI,SAAS,QAAQ,UAAU;AAE/B,MAAI,gBAAgB;AACpB,MAAI,iBAAkC,CAAC;AACvC,MAAI,WAAiC;AACrC,MAAI,WAAiC;AACrC,MAAI,oBAAmC,QAAQ,QAAQ;AACvD,MAAI,qBAAqB;AACzB,QAAM,mBAAmB,QAAQ,oBAAoB,IAAI,OAAO;AAChE,QAAM,iBAAiB,QAAQ,kBAAkB,mBAAmB,KAAK,IAAI,KAAK,MAAM,KAAK,KAAK,mBAAmB,EAAE,CAAC;AAExH,QAAM,YAAY,CAAC,UAAwE;AACzF,YAAQ,UAAU,EAAE,GAAG,OAAO,YAAY,QAAQ,YAAY,QAAQ,QAAQ,KAAK,GAAG,CAAC;AAAA,EACzF;AAEA,QAAM,iBAAiB,QAAQ,WAAW,SAAS,EAAE;AACrD,QAAM,qBAAqB,QAAQ;AACnC,QAAM,kBAAkB,QAAQ,aAAa;AAC7C,QAAM,mBAAmB,OAAO,QAAQ,mBAAmB,CAAC;AAC5D,QAAM,qBAAqB,KAAK,UAAU,QAAQ,WAAW,WAAW,WAAW,IAAI,CAAC,eAAe,EAAE,MAAM,UAAU,MAAM,SAAS,UAAU,QAAQ,EAAE,CAAC;AAC7J,QAAM,eAAe,CAAC,KAAa,UAAkB,SAAuB;AAC1E,UAAM,UAAU,SAAS,IAAI,GAAG;AAChC,QAAI,YAAY,UAAa,YAAY,SAAU,OAAM,IAAI,MAAM,GAAG,IAAI,IAAI,OAAO,IAAI,QAAQ,EAAE;AACnG,QAAI,YAAY,OAAW,UAAS,IAAI,KAAK,QAAQ;AAAA,EACvD;AACA,eAAa,cAAc,gBAAgB,6BAA6B;AACxE,eAAa,cAAc,oBAAoB,+BAA+B;AAC9E,eAAa,aAAa,iBAAiB,8BAA8B;AACzE,eAAa,mBAAmB,kBAAkB,+BAA+B;AACjF,eAAa,cAAc,oBAAoB,iCAAiC;AAEhF,QAAM,iBAAiB,CAAC,cAA2B;AACjD,UAAM,oBAAoB,UAAU,OAAe,UAAU;AAC7D,UAAM,SAAS,kBAAkB,IAAI,YAAY;AACjD,QAAI,WAAW,eAAgB,OAAM,IAAI,MAAM,+BAA+B,UAAU,SAAS,IAAI,cAAc,EAAE;AACrH,QAAI,kBAAkB,IAAI,YAAY,MAAM,mBAAoB,OAAM,IAAI,MAAM,+BAA+B;AAC/G,QAAI,kBAAkB,IAAI,WAAW,MAAM,gBAAiB,OAAM,IAAI,MAAM,8BAA8B;AAC1G,QAAI,kBAAkB,IAAI,iBAAiB,MAAM,iBAAkB,OAAM,IAAI,MAAM,+BAA+B;AAClH,QAAI,kBAAkB,IAAI,YAAY,MAAM,mBAAoB,OAAM,IAAI,MAAM,iCAAiC;AACjH,UAAM,WAAW,UAAU,QAAQ,UAAU,EAAE,SAAS;AACxD,QAAI;AACF,cAAQ,WAAW,iBAAiB,QAAQ;AAAA,IAC9C,SAAS,OAAO;AACd,YAAM,IAAI,MAAM,gCAAgC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,EAAE;AAAA,IAC1G;AACA,iBAAa,UAAU,OAAe,SAAS,EAAE,IAAI,OAAO,GAAG,SAAS,MAAM;AAAA,EAChF;AAEA,QAAM,gBAAgB,CAAC,UAA4B;AACjD,QAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,aAAa;AACxD,QAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,WAAO,OAAO;AAAA,MACZ,OAAO,QAAQ,KAAgC,EAC5C,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,MAAM,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAC,EAClE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,cAAc,KAAK,CAAC,CAAC;AAAA,IACtD;AAAA,EACF;AACA,QAAM,mBAAmB,CAAC,UAA2C,KAAK,UAAU,cAAc;AAAA,IAChG,UAAU,OAAO,YAAY,CAAC;AAAA,IAC9B,aAAa,OAAO,eAAe,CAAC;AAAA,IACpC,aAAa,OAAO,eAAe;AAAA,IACnC,OAAO,OAAO,SAAS,CAAC;AAAA,EAC1B,CAAC,CAAC;AAEF,QAAM,iBAAiB,OAAO,QAAoB,WAAqC;AACrF,UAAM,WAAW;AACjB,UAAM,YAAY,IAAM,MAAI,EAAE,MAAM,mBAAmB,CAAC;AACxD,QAAI;AACF,MAAE,cAAY,WAAa,sBAAoB,GAAG,GAAG,qBAAqB;AAC1E,MAAE,cAAY,WAAW,QAAQ,sBAAsB;AACvD,qBAAe,SAAS;AACxB,YAAM,kBAAkB,aAAa,QAAQ,IAAI,OAAO,GAAG,OAAO,MAAM;AACxE,YAAM,mBAAmB,aAAa,UAAU,OAAe,SAAS,EAAE,IAAI,OAAO,GAAG,UAAU,QAAQ,UAAU,EAAE,MAAM;AAC5H,UAAI,iBAAiB,eAAe,MAAM,iBAAiB,gBAAgB,GAAG;AAC5E,YAAI,CAAC,QAAQ,0BAA0B,CAAE,MAAM,QAAQ,uBAAuB,iBAAiB,kBAAkB,MAAM,GAAI;AACzH,gBAAM,IAAI,MAAM,wCAAwC;AAAA,QAC1D;AAAA,MACF;AACA,aAAO;AAAA,IACT,UAAE;AACA,gBAAU,QAAQ;AAAA,IACpB;AAAA,EACF;AAEA,MAAI,CAAC,2BAA2B;AAC9B,QAAI,OAAO,WAAW,EAAG,QAAO,OAAO,GAAG,QAAQ,WAAW,SAAS,EAAE,QAAQ;AAChF,QAAI,CAAC,QAAQ,IAAI,OAAO,KAAK,QAAQ,WAAW,SAAS,EAAE,QAAS,SAAQ,IAAI,SAAS,KAAK,UAAU,QAAQ,WAAW,SAAS,EAAE,OAAO,CAAC;AAAA,EAChJ,OAAO;AACL,mBAAe,GAAG;AAClB,UAAM,QAAQ,QAAQ,WAAW,SAAS;AAC1C,UAAM,iBAAiB,OAAO,SAAS;AACvC,UAAM,gBAAgB,aAAa,QAAQ,IAAI,OAAO,GAAG,eAAe,MAAM;AAC9E,QAAI,UAAU,IAAI,YAAY,EAAE,WAAW,gBAAgB,EAAE,aAAa,EAAE,cAAc,MAAM,CAAC;AACjG,QAAI,mBAAmB,MAAM,SAAU,WAAU,QAAQ,cAAc,GAAG,MAAM,SAAS,QAAQ,cAAc;AAC/G,QAAI,KAAK,UAAU,aAAa,MAAM,KAAK,UAAU,MAAM,OAAO,EAAG,WAAU,QAAQ,WAAW,aAAa;AAC/G,QAAI,QAAQ,WAAW,SAAS,EAAG,SAAQ,WAAW,SAAS,OAAO;AAAA,EACxE;AAEA,YAAU,cAAc,EAAE,MAAM,QAAQ,MAAM,YAAY,QAAQ,WAAW,CAAC;AAE9E,QAAM,0BAA0B,MAAY;AAAE,0BAAsB;AAAA,EAAG;AACvE,MAAI,GAAG,oBAAoB,uBAAuB;AAElD,QAAM,cAAc,QAAQ,WAAW,UAAU,CAAC,WAAW;AAC3D,QAAI,kBAAkB,UAAW;AACjC,QAAI,SAAS,MAAM;AACjB,UAAI,OAAO,iBAAiB;AAC1B,cAAM,UAAU,OAAO,SAAS;AAChC,cAAM,QAAQ,aAAa,SAAS,OAAO,QAAQ,QAAQ;AAC3D,YAAI,MAAM,KAAK,MAAM,KAAM,QAAO,OAAO,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI;AAC1E,YAAI,MAAM,SAAU,QAAO,OAAO,MAAM,MAAM,MAAM,QAAQ;AAAA,MAC9D;AACA,UAAI,OAAO,gBAAgB;AACzB,YAAI,OAAO,QAAQ,QAAS,SAAQ,IAAI,SAAS,KAAK,UAAU,OAAO,QAAQ,OAAO,CAAC;AAAA,YAClF,SAAQ,OAAO,OAAO;AAAA,MAC7B;AAAA,IACF,GAAG,WAAW;AAAA,EAChB,CAAC;AAED,QAAM,mBAAmB,CAAC,gBAAqC;AAC7D,QAAI,aAAa,YAAY,WAAW,YAAa;AACrD,qBAAiB;AACjB,QAAI;AACF,YAAM,QAAQ,QAAQ,WAAW,SAAS;AAC1C,YAAM,aAAa,OAAO,SAAS;AACnC,UAAI,OAAO,IAAI,YAAY,EAAE,WAAW,eAAe,EAAE,aAAa,EAAE,cAAc,MAAM,CAAC;AAC7F,UAAI,eAAe,MAAM,SAAU,QAAO,KAAK,cAAc,GAAG,MAAM,SAAS,QAAQ,UAAU;AACjG,qBAAe,GAAG;AAClB,YAAM,cAAc,aAAa,QAAQ,IAAI,OAAO,GAAG,WAAW,MAAM;AACxE,UAAI,KAAK,UAAU,WAAW,MAAM,KAAK,UAAU,MAAM,OAAO,EAAG,QAAO,KAAK,WAAW,WAAW;AACrG,UAAI,KAAK,WAAW,SAAS,EAAG,SAAQ,WAAW,SAAS,IAAI;AAAA,IAClE,UAAE;AAAU,uBAAiB;AAAA,IAAO;AAAA,EACtC;AACA,MAAI,GAAG,oBAAoB,gBAAgB;AAE3C,QAAM,iBAAiB,MAAkB;AACvC,UAAM,cAAc,eAAe,OAAO,CAAC,UAAU,UAAU,QAAQ;AACvE,UAAM,SAAW,eAAa,YAAY,IAAI,CAAC,UAAU,MAAM,MAAM,CAAC;AACtE,QAAI,OAAO,aAAa,gBAAgB;AACtC,YAAM,IAAI,WAAW,gCAAgC,cAAc,EAAE;AAAA,IACvE;AACA,QAAI,YAAY,WAAW,EAAG,QAAO;AACrC,UAAM,cAAc,EAAE,IAAI,EAAE,eAAe,OAAO;AAClD,qBAAiB,WAAW,CAAC,UAAU,WAAW,IAAI,CAAC,WAAW;AAClE,cAAU,EAAE,MAAM,WAAW,YAAY,OAAO,YAAY,SAAS,eAAe,OAAO,CAAC;AAC5F,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,YAA2B;AACvC,QAAI,UAAW,OAAM,IAAI,MAAM,uBAAuB;AACtD,QAAI,CAAC,UAAU,CAAC,QAAQ,cAAc,eAAe,WAAW,EAAG;AACnE,QAAI,SAAU,QAAO;AACrB,gBAAY,YAAY;AACtB,aAAO,UAAU,eAAe,SAAS,GAAG;AAC1C,cAAM,QAAQ,eAAe,CAAC;AAC9B,mBAAW;AACX,YAAI;AACF,gBAAM,QAAQ,WAAY,MAAM,MAAM;AACtC,2BAAiB,eAAe,OAAO,CAAC,cAAc,cAAc,KAAK;AACzE,oBAAU,EAAE,MAAM,SAAS,YAAY,MAAM,OAAO,YAAY,SAAS,eAAe,OAAO,CAAC;AAAA,QAClG,UAAE;AACA,cAAI,aAAa,MAAO,YAAW;AAAA,QACrC;AAAA,MACF;AAAA,IACF,GAAG,EAAE,QAAQ,MAAM;AAAE,iBAAW;AAAA,IAAM,CAAC;AACvC,WAAO;AAAA,EACT;AAEA,QAAM,mBAAmB,CAAC,QAAoB,WAA0B;AACtE,QAAI,aAAa,WAAW,YAAa;AACzC,mBAAe,KAAK,EAAE,IAAI,EAAE,eAAe,QAAQ,OAAO,MAAM,EAAE,CAAC;AACnE,cAAU,EAAE,MAAM,gBAAgB,YAAY,OAAO,YAAY,SAAS,eAAe,OAAO,CAAC;AACjG,UAAM,YAAY,QAAQ,4BAA4B,IAAI,OAAO;AACjE,QAAI,eAAe,OAAO,CAAC,OAAO,UAAU,QAAQ,MAAM,OAAO,YAAY,CAAC,IAAI,WAAW;AAC3F,UAAI;AACF,uBAAe;AAAA,MACjB,SAAS,OAAO;AACd,YAAI,EAAE,iBAAiB,eAAe,CAAC,MAAM,QAAQ,WAAW,+BAA+B,EAAG,OAAM;AAAA,MAC1G;AAAA,IACF;AACA,QAAI,OAAQ,MAAK,MAAM,EAAE,MAAM,CAAC,UAAmB;AACjD,eAAS;AACT,gBAAU,EAAE,MAAM,WAAW,SAAS,eAAe,QAAQ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;AAAA,IAC9H,CAAC;AAAA,EACH;AACA,MAAI,GAAG,UAAU,gBAAgB;AACjC,MAAI,QAAQ,YAAY;AACtB,UAAM,UAAY,sBAAoB,GAAG;AACzC,mBAAe,KAAK,EAAE,IAAI,EAAE,eAAe,QAAQ,QAAQ,CAAC;AAC5D,cAAU,EAAE,MAAM,gBAAgB,YAAY,QAAQ,YAAY,SAAS,eAAe,OAAO,CAAC;AAClG,QAAI,OAAQ,MAAK,MAAM,EAAE,MAAM,CAAC,UAAmB;AACjD,eAAS;AACT,gBAAU,EAAE,MAAM,WAAW,SAAS,eAAe,QAAQ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;AAAA,IAC9H,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAc,CAAC,gBAAgB,cAAgB,sBAAoB,KAAK,WAAW,IAAM,sBAAoB,GAAG;AAAA,IAChH,mBAAmB,MAAQ,oBAAkB,GAAG;AAAA,IAChD,MAAM,YAAY,QAAQ,SAAS,aAAa;AAC9C,UAAI,UAAW,OAAM,IAAI,MAAM,uBAAuB;AACtD,YAAM,QAAQ,YAA2B;AACvC,YAAI;AACF,cAAI,UAAW,OAAM,IAAI,MAAM,uBAAuB;AACtD,cAAI,OAAO,aAAa,eAAgB,OAAM,IAAI,WAAW,gCAAgC,cAAc,EAAE;AAC7G,cAAI,QAAQ,mBAAmB,CAAE,MAAM,QAAQ,gBAAgB,QAAQ,MAAM,EAAI,OAAM,IAAI,MAAM,8BAA8B;AAC/H,qBAAS;AACP,kBAAM,WAAW,MAAM,eAAe,QAAQ,MAAM;AACpD,gBAAI,aAAa,mBAAoB;AACrC,gBAAI,UAAW,OAAM,IAAI,MAAM,uBAAuB;AACtD,YAAE,cAAY,KAAK,QAAQ,MAAM;AACjC,sBAAU,EAAE,MAAM,iBAAiB,YAAY,OAAO,WAAW,CAAC;AAClE;AAAA,UACF;AAAA,QACF,SAAS,OAAO;AACd,oBAAU,EAAE,MAAM,mBAAmB,YAAY,OAAO,YAAY,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;AACnI,gBAAM;AAAA,QACR;AAAA,MACF;AACA,YAAM,SAAS,kBAAkB,KAAK,OAAO,KAAK;AAClD,0BAAoB,OAAO,MAAM,MAAM,MAAS;AAChD,aAAO;AAAA,IACT;AAAA,IACA,YAAY,OAAO;AAAE,UAAI,UAAW,OAAM,IAAI,MAAM,uBAAuB;AAAG,gBAAU,mBAAmB,YAAY,KAAK;AAAA,IAAG;AAAA,IAC/H,aAAa,MAAM,UAAU,UAAU;AAAA,IACvC,UAAU,MAAM;AAAA,IAChB,oBAAoB,MAAM,eAAe;AAAA,IACzC,MAAM,UAAU,OAAO;AACrB,UAAI,UAAW,OAAM,IAAI,MAAM,uBAAuB;AACtD,eAAS;AACT,gBAAU,EAAE,MAAM,QAAQ,WAAW,WAAW,SAAS,eAAe,OAAO,CAAC;AAChF,UAAI,MAAO,OAAM,MAAM;AAAA,IACzB;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,UAAU;AACR,UAAI,UAAW;AACf,kBAAY;AACZ,kBAAY;AACZ,UAAI,IAAI,oBAAoB,gBAAgB;AAC5C,UAAI,IAAI,oBAAoB,uBAAuB;AACnD,UAAI,IAAI,UAAU,gBAAgB;AAClC,uBAAiB,CAAC;AAClB,gBAAU,cAAc,IAAI;AAC5B,UAAI,cAAe,WAAU,QAAQ;AAAA,IACvC;AAAA,EACF;AACF;AAGO,IAAM,sBAAsB;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mdfn/collab", "version": "0.1.0", "description": "Yjs collaboration binding for mdfn source, presence, and sidecar state", "type": "module",
|
|
3
|
+
"main": "dist/index.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } },
|
|
4
|
+
"scripts": { "build": "tsup src/index.ts --format esm,cjs --dts --sourcemap --clean", "test": "vitest run", "typecheck": "tsc --noEmit -p tsconfig.json", "lint": "tsc --noEmit -p tsconfig.json", "prepack": "npm run build" },
|
|
5
|
+
"dependencies": { "@mdfn/core": "0.1.0", "y-protocols": "^1.0.6", "yjs": "^13.6.32" },
|
|
6
|
+
"devDependencies": { "@mdfn/markdown": "0.1.0", "tsup": "^8.0.0", "typescript": "^5.3.0", "vitest": "3.2.4" },
|
|
7
|
+
"files": ["dist", "README.md"], "sideEffects": false, "license": "MIT", "engines": { "node": ">=20 <25" }, "repository": { "type": "git", "url": "git+https://github.com/21nCo/super-functions.git", "directory": "mdfn/collab" }, "publishConfig": { "access": "public" },
|
|
8
|
+
"mdfn": { "status": "optional", "layer": "platform", "schemaVersion": 1 }
|
|
9
|
+
}
|