leafrelay 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-Q3R5N4YE.js → chunk-OZ4FLSVP.js} +770 -123
- package/dist/chunk-OZ4FLSVP.js.map +1 -0
- package/dist/{chunk-2EXJYAER.js → chunk-Y543FNFD.js} +11 -8
- package/dist/chunk-Y543FNFD.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/daemon.js +57 -61
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-2EXJYAER.js.map +0 -1
- package/dist/chunk-Q3R5N4YE.js.map +0 -1
|
@@ -18,10 +18,12 @@ import { createHash } from "crypto";
|
|
|
18
18
|
import { posix } from "path";
|
|
19
19
|
import DiffMatchPatch from "diff-match-patch";
|
|
20
20
|
import { posix as posix2 } from "path";
|
|
21
|
+
import DiffMatchPatch2 from "diff-match-patch";
|
|
21
22
|
import { createHash as createHash2 } from "crypto";
|
|
22
23
|
import { diff3Merge } from "node-diff3";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
24
|
+
import { randomUUID } from "crypto";
|
|
25
|
+
import { mkdir as mkdir3, readFile as readFile4, readdir as readdir2, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
26
|
+
import { dirname as dirname3, join as join2, resolve as resolve4 } from "path";
|
|
25
27
|
var PACKET_TYPES = [
|
|
26
28
|
"disconnect",
|
|
27
29
|
"connect",
|
|
@@ -152,6 +154,7 @@ var OverleafRealtimeSocket = class {
|
|
|
152
154
|
cookie;
|
|
153
155
|
socket;
|
|
154
156
|
connected = false;
|
|
157
|
+
projectJoined = false;
|
|
155
158
|
explicitlyDisconnected = false;
|
|
156
159
|
reconnectEnabled;
|
|
157
160
|
reconnectAttempts = 0;
|
|
@@ -160,7 +163,6 @@ var OverleafRealtimeSocket = class {
|
|
|
160
163
|
closeTimeoutMs = 6e4;
|
|
161
164
|
acknowledgementId = 0;
|
|
162
165
|
acknowledgements = /* @__PURE__ */ new Map();
|
|
163
|
-
sendBuffer = [];
|
|
164
166
|
io = {
|
|
165
167
|
reconnect: (enabled) => {
|
|
166
168
|
this.reconnectEnabled = enabled;
|
|
@@ -192,15 +194,24 @@ var OverleafRealtimeSocket = class {
|
|
|
192
194
|
if (id && callback) {
|
|
193
195
|
this.acknowledgements.set(id, callback);
|
|
194
196
|
}
|
|
195
|
-
|
|
197
|
+
const packet = encodePacket({
|
|
196
198
|
type: "event",
|
|
197
199
|
id,
|
|
198
200
|
ack: callback ? "data" : void 0,
|
|
199
201
|
name: event,
|
|
200
202
|
args
|
|
201
|
-
})
|
|
203
|
+
});
|
|
204
|
+
if (!this.sendApplicationEvent(packet) && callback) {
|
|
205
|
+
if (id) {
|
|
206
|
+
this.acknowledgements.delete(id);
|
|
207
|
+
}
|
|
208
|
+
queueMicrotask(() => callback(new Error("The Overleaf project session is not connected.")));
|
|
209
|
+
}
|
|
202
210
|
return this;
|
|
203
211
|
}
|
|
212
|
+
markProjectJoined() {
|
|
213
|
+
this.projectJoined = true;
|
|
214
|
+
}
|
|
204
215
|
disconnect() {
|
|
205
216
|
this.explicitlyDisconnected = true;
|
|
206
217
|
this.reconnectEnabled = false;
|
|
@@ -215,6 +226,7 @@ var OverleafRealtimeSocket = class {
|
|
|
215
226
|
}
|
|
216
227
|
async connect() {
|
|
217
228
|
try {
|
|
229
|
+
this.projectJoined = false;
|
|
218
230
|
const handshakeUrl = new URL("/socket.io/1/", this.endpoint.origin);
|
|
219
231
|
for (const [name, value] of this.endpoint.searchParams) {
|
|
220
232
|
handshakeUrl.searchParams.set(name, value);
|
|
@@ -243,7 +255,7 @@ var OverleafRealtimeSocket = class {
|
|
|
243
255
|
this.socket.addEventListener("open", () => this.events.emit("connecting", "websocket"));
|
|
244
256
|
this.socket.addEventListener("message", (event) => void this.receive(event.data));
|
|
245
257
|
this.socket.addEventListener("error", (error2) => this.events.emit("error", error2));
|
|
246
|
-
this.socket.addEventListener("close", () => this.handleClose());
|
|
258
|
+
this.socket.addEventListener("close", (event) => this.handleClose(event));
|
|
247
259
|
} catch (error2) {
|
|
248
260
|
this.events.emit("connect_failed", error2);
|
|
249
261
|
this.events.emit("error", error2);
|
|
@@ -263,7 +275,6 @@ var OverleafRealtimeSocket = class {
|
|
|
263
275
|
const wasReconnecting = this.reconnectAttempts > 0;
|
|
264
276
|
this.connected = true;
|
|
265
277
|
this.events.emit("connect");
|
|
266
|
-
this.flush();
|
|
267
278
|
if (wasReconnecting) {
|
|
268
279
|
this.events.emit("reconnect", "websocket", this.reconnectAttempts);
|
|
269
280
|
this.reconnectAttempts = 0;
|
|
@@ -309,16 +320,16 @@ var OverleafRealtimeSocket = class {
|
|
|
309
320
|
}
|
|
310
321
|
}
|
|
311
322
|
send(packet) {
|
|
312
|
-
if (
|
|
313
|
-
this.
|
|
314
|
-
return;
|
|
323
|
+
if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
|
|
324
|
+
this.socket.send(packet);
|
|
315
325
|
}
|
|
316
|
-
this.socket.send(packet);
|
|
317
326
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
327
|
+
sendApplicationEvent(packet) {
|
|
328
|
+
if (!this.connected || !this.projectJoined || this.socket?.readyState !== WebSocket.OPEN) {
|
|
329
|
+
return false;
|
|
321
330
|
}
|
|
331
|
+
this.socket.send(packet);
|
|
332
|
+
return true;
|
|
322
333
|
}
|
|
323
334
|
resetInactivityTimer() {
|
|
324
335
|
if (this.inactivityTimer) {
|
|
@@ -326,20 +337,25 @@ var OverleafRealtimeSocket = class {
|
|
|
326
337
|
}
|
|
327
338
|
this.inactivityTimer = setTimeout(() => this.closeSocket("timeout"), this.closeTimeoutMs);
|
|
328
339
|
}
|
|
329
|
-
handleClose() {
|
|
340
|
+
handleClose(event) {
|
|
330
341
|
const wasConnected = this.connected;
|
|
331
342
|
this.connected = false;
|
|
343
|
+
this.projectJoined = false;
|
|
332
344
|
if (this.inactivityTimer) {
|
|
333
345
|
clearTimeout(this.inactivityTimer);
|
|
334
346
|
}
|
|
335
347
|
if (wasConnected) {
|
|
336
|
-
|
|
348
|
+
const detail = event.reason ? `: ${event.reason}` : "";
|
|
349
|
+
const reason = `transport close (code ${event.code})${detail}`;
|
|
350
|
+
this.failAcknowledgements(reason);
|
|
351
|
+
this.events.emit("disconnect", reason);
|
|
337
352
|
}
|
|
338
353
|
this.scheduleReconnect();
|
|
339
354
|
}
|
|
340
355
|
closeSocket(reason) {
|
|
341
356
|
const wasConnected = this.connected;
|
|
342
357
|
this.connected = false;
|
|
358
|
+
this.projectJoined = false;
|
|
343
359
|
if (this.inactivityTimer) {
|
|
344
360
|
clearTimeout(this.inactivityTimer);
|
|
345
361
|
}
|
|
@@ -347,12 +363,20 @@ var OverleafRealtimeSocket = class {
|
|
|
347
363
|
this.socket = void 0;
|
|
348
364
|
socket?.close();
|
|
349
365
|
if (wasConnected) {
|
|
366
|
+
this.failAcknowledgements(reason);
|
|
350
367
|
this.events.emit("disconnect", reason);
|
|
351
368
|
}
|
|
352
369
|
if (!this.explicitlyDisconnected) {
|
|
353
370
|
this.scheduleReconnect();
|
|
354
371
|
}
|
|
355
372
|
}
|
|
373
|
+
failAcknowledgements(reason) {
|
|
374
|
+
const error2 = new Error(`Overleaf realtime connection closed: ${reason}`);
|
|
375
|
+
for (const callback of this.acknowledgements.values()) {
|
|
376
|
+
callback(error2);
|
|
377
|
+
}
|
|
378
|
+
this.acknowledgements.clear();
|
|
379
|
+
}
|
|
356
380
|
scheduleReconnect() {
|
|
357
381
|
if (!this.reconnectEnabled || this.explicitlyDisconnected || this.reconnectTimer) {
|
|
358
382
|
return;
|
|
@@ -1188,6 +1212,8 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1188
1212
|
static alternativeSocketFactory;
|
|
1189
1213
|
scheme;
|
|
1190
1214
|
record;
|
|
1215
|
+
joinedProject;
|
|
1216
|
+
pendingProjectJoin;
|
|
1191
1217
|
_handlers = [];
|
|
1192
1218
|
connectedPublicId;
|
|
1193
1219
|
socket;
|
|
@@ -1224,6 +1250,8 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1224
1250
|
break;
|
|
1225
1251
|
case "realtime":
|
|
1226
1252
|
this.record = void 0;
|
|
1253
|
+
this.joinedProject = void 0;
|
|
1254
|
+
this.resetProjectJoin(new Error("The realtime connection was replaced."));
|
|
1227
1255
|
const query = `?projectId=${this.projectId}&t=${Date.now()}`;
|
|
1228
1256
|
this.socket = this.api._initSocketV0(this.identity, query);
|
|
1229
1257
|
break;
|
|
@@ -1266,6 +1294,11 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1266
1294
|
});
|
|
1267
1295
|
this.socket.on("disconnect", (reason) => {
|
|
1268
1296
|
log("SocketIOAPI: disconnect event", { scheme: this.scheme, projectId: this.projectId, reason });
|
|
1297
|
+
if (this.scheme === "realtime") {
|
|
1298
|
+
this.joinedProject = void 0;
|
|
1299
|
+
this.connectedPublicId = void 0;
|
|
1300
|
+
this.resetProjectJoin(new Error(`Overleaf realtime connection closed: ${String(reason ?? "unknown")}`));
|
|
1301
|
+
}
|
|
1269
1302
|
});
|
|
1270
1303
|
this.socket.on("reconnecting", (delay, attempt) => {
|
|
1271
1304
|
log("SocketIOAPI: reconnecting", { scheme: this.scheme, projectId: this.projectId, delay, attempt });
|
|
@@ -1290,21 +1323,49 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1290
1323
|
error(`Overleaf connection error: ${message}`);
|
|
1291
1324
|
});
|
|
1292
1325
|
if (this.scheme === "realtime") {
|
|
1293
|
-
this.
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
}
|
|
1326
|
+
this.pendingProjectJoin ??= this.createProjectJoin();
|
|
1327
|
+
this.socket.on("joinProjectResponse", (res) => {
|
|
1328
|
+
const publicId = res.publicId;
|
|
1329
|
+
const project = res.project;
|
|
1330
|
+
this.socket.markProjectJoined?.();
|
|
1331
|
+
log("SocketIOAPI: project joined", { scheme: this.scheme, projectId: this.projectId, publicId });
|
|
1332
|
+
this.connectedPublicId = publicId;
|
|
1333
|
+
this.joinedProject = project;
|
|
1334
|
+
this.pendingProjectJoin?.resolve(project);
|
|
1335
|
+
for (const handler of this._handlers) {
|
|
1336
|
+
handler.onProjectJoined?.(project);
|
|
1337
|
+
handler.onConnectionAccepted?.(publicId);
|
|
1338
|
+
}
|
|
1306
1339
|
});
|
|
1340
|
+
this.socket.on("connectionRejected", (err) => {
|
|
1341
|
+
this.pendingProjectJoin?.reject(err?.message || err);
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
createProjectJoin() {
|
|
1346
|
+
let resolve6;
|
|
1347
|
+
let reject;
|
|
1348
|
+
const promise = new Promise((resolveProject, rejectProject) => {
|
|
1349
|
+
resolve6 = resolveProject;
|
|
1350
|
+
reject = rejectProject;
|
|
1351
|
+
});
|
|
1352
|
+
void promise.catch(() => void 0);
|
|
1353
|
+
return { promise, resolve: resolve6, reject };
|
|
1354
|
+
}
|
|
1355
|
+
resetProjectJoin(reason) {
|
|
1356
|
+
if (reason !== void 0) {
|
|
1357
|
+
this.pendingProjectJoin?.reject(reason);
|
|
1358
|
+
}
|
|
1359
|
+
this.pendingProjectJoin = this.createProjectJoin();
|
|
1360
|
+
}
|
|
1361
|
+
waitForRealtimeProject() {
|
|
1362
|
+
return this.joinedProject ? Promise.resolve(this.joinedProject) : this.pendingProjectJoin.promise;
|
|
1363
|
+
}
|
|
1364
|
+
async emitProjectEvent(event, ...args) {
|
|
1365
|
+
if (this.scheme === "realtime") {
|
|
1366
|
+
await this.waitForRealtimeProject();
|
|
1307
1367
|
}
|
|
1368
|
+
return this.emit(event, ...args);
|
|
1308
1369
|
}
|
|
1309
1370
|
disconnect() {
|
|
1310
1371
|
this.socket.disconnect();
|
|
@@ -1364,10 +1425,12 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1364
1425
|
});
|
|
1365
1426
|
break;
|
|
1366
1427
|
case handlers.onDisconnected:
|
|
1367
|
-
this.socket.on("disconnect", () => {
|
|
1368
|
-
handler();
|
|
1428
|
+
this.socket.on("disconnect", (reason) => {
|
|
1429
|
+
handler(reason);
|
|
1369
1430
|
});
|
|
1370
1431
|
break;
|
|
1432
|
+
case handlers.onProjectJoined:
|
|
1433
|
+
break;
|
|
1371
1434
|
case handlers.onConnectionAccepted:
|
|
1372
1435
|
this.socket.on("connectionAccepted", (_, publicId) => {
|
|
1373
1436
|
handler(publicId);
|
|
@@ -1437,7 +1500,7 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1437
1500
|
timeoutPromise
|
|
1438
1501
|
]);
|
|
1439
1502
|
case "realtime":
|
|
1440
|
-
return Promise.race([this.
|
|
1503
|
+
return Promise.race([this.waitForRealtimeProject(), timeoutPromise]);
|
|
1441
1504
|
}
|
|
1442
1505
|
}
|
|
1443
1506
|
/**
|
|
@@ -1446,7 +1509,7 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1446
1509
|
* @returns {Promise}
|
|
1447
1510
|
*/
|
|
1448
1511
|
async joinDoc(docId) {
|
|
1449
|
-
return this.
|
|
1512
|
+
return this.emitProjectEvent("joinDoc", docId, { encodeRanges: true }).then((returns) => {
|
|
1450
1513
|
const [docLinesAscii, version, updates, ranges] = returns;
|
|
1451
1514
|
const docLines = docLinesAscii.map((line) => decodePackedUtf8(line));
|
|
1452
1515
|
return { docLines, version, updates, ranges };
|
|
@@ -1458,7 +1521,7 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1458
1521
|
* @returns {Promise}
|
|
1459
1522
|
*/
|
|
1460
1523
|
async leaveDoc(docId) {
|
|
1461
|
-
return this.
|
|
1524
|
+
return this.emitProjectEvent("leaveDoc", docId).then(() => {
|
|
1462
1525
|
return;
|
|
1463
1526
|
});
|
|
1464
1527
|
}
|
|
@@ -1470,7 +1533,7 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1470
1533
|
*/
|
|
1471
1534
|
async applyOtUpdate(docId, update) {
|
|
1472
1535
|
try {
|
|
1473
|
-
await this.
|
|
1536
|
+
await this.emitProjectEvent("applyOtUpdate", docId, update);
|
|
1474
1537
|
} catch (error2) {
|
|
1475
1538
|
let detail;
|
|
1476
1539
|
if (error2 instanceof Error) {
|
|
@@ -1492,7 +1555,7 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1492
1555
|
* @returns {Promise}
|
|
1493
1556
|
*/
|
|
1494
1557
|
async getConnectedUsers() {
|
|
1495
|
-
return this.
|
|
1558
|
+
return this.emitProjectEvent("clientTracking.getConnectedUsers").then((returns) => {
|
|
1496
1559
|
const [connectedUsers] = returns;
|
|
1497
1560
|
return connectedUsers;
|
|
1498
1561
|
});
|
|
@@ -1503,7 +1566,7 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1503
1566
|
* @returns {Promise}
|
|
1504
1567
|
*/
|
|
1505
1568
|
async updatePosition(doc_id, row, column) {
|
|
1506
|
-
return this.
|
|
1569
|
+
return this.emitProjectEvent("clientTracking.updatePosition", { row, column, doc_id }).then(() => {
|
|
1507
1570
|
return;
|
|
1508
1571
|
});
|
|
1509
1572
|
}
|
|
@@ -1767,7 +1830,9 @@ var RemoteProject = class {
|
|
|
1767
1830
|
project;
|
|
1768
1831
|
entries = /* @__PURE__ */ new Map();
|
|
1769
1832
|
idToPath = /* @__PURE__ */ new Map();
|
|
1833
|
+
collaboratorNames = /* @__PURE__ */ new Map();
|
|
1770
1834
|
changeHandler;
|
|
1835
|
+
reconnectHandler;
|
|
1771
1836
|
externalTransport;
|
|
1772
1837
|
async connect() {
|
|
1773
1838
|
if (this.externalTransport) {
|
|
@@ -1782,6 +1847,7 @@ var RemoteProject = class {
|
|
|
1782
1847
|
this.socket = new SocketIOAPI(this.url, this.api, this.identity, this.projectId);
|
|
1783
1848
|
}
|
|
1784
1849
|
this.project = await this.socket.joinProject(this.projectId);
|
|
1850
|
+
this.indexCollaborators();
|
|
1785
1851
|
this.reindex();
|
|
1786
1852
|
this.registerEvents();
|
|
1787
1853
|
}
|
|
@@ -1792,6 +1858,19 @@ var RemoteProject = class {
|
|
|
1792
1858
|
}
|
|
1793
1859
|
onChange(handler) {
|
|
1794
1860
|
this.changeHandler = handler;
|
|
1861
|
+
return () => {
|
|
1862
|
+
if (this.changeHandler === handler) {
|
|
1863
|
+
this.changeHandler = void 0;
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
onReconnect(handler) {
|
|
1868
|
+
this.reconnectHandler = handler;
|
|
1869
|
+
return () => {
|
|
1870
|
+
if (this.reconnectHandler === handler) {
|
|
1871
|
+
this.reconnectHandler = void 0;
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1795
1874
|
}
|
|
1796
1875
|
listEntries() {
|
|
1797
1876
|
return [...this.entries.values()];
|
|
@@ -2006,11 +2085,40 @@ var RemoteProject = class {
|
|
|
2006
2085
|
collection.splice(index, 1);
|
|
2007
2086
|
}
|
|
2008
2087
|
}
|
|
2009
|
-
emitChange(path, previousPath) {
|
|
2010
|
-
this.changeHandler?.({ path: normalizePath(path), previousPath });
|
|
2088
|
+
emitChange(path, previousPath, actor) {
|
|
2089
|
+
this.changeHandler?.({ path: normalizePath(path), previousPath, actor });
|
|
2090
|
+
}
|
|
2091
|
+
indexCollaborators() {
|
|
2092
|
+
for (const member of [this.project.owner, ...this.project.members]) {
|
|
2093
|
+
if (!member?._id) {
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
const name = [member.first_name, member.last_name].filter(Boolean).join(" ").trim();
|
|
2097
|
+
if (name) {
|
|
2098
|
+
this.collaboratorNames.set(member._id, name);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
actor(userId) {
|
|
2103
|
+
if (!userId) {
|
|
2104
|
+
return void 0;
|
|
2105
|
+
}
|
|
2106
|
+
const name = this.collaboratorNames.get(userId);
|
|
2107
|
+
return name ? `${name} (Overleaf)` : `Overleaf user ${userId}`;
|
|
2011
2108
|
}
|
|
2012
2109
|
registerEvents() {
|
|
2013
2110
|
this.socket.updateEventHandlers({
|
|
2111
|
+
onProjectJoined: (project) => {
|
|
2112
|
+
this.project = project;
|
|
2113
|
+
this.indexCollaborators();
|
|
2114
|
+
this.reindex();
|
|
2115
|
+
this.reconnectHandler?.();
|
|
2116
|
+
},
|
|
2117
|
+
onClientUpdated: (user) => {
|
|
2118
|
+
if (user.user_id && user.name) {
|
|
2119
|
+
this.collaboratorNames.set(user.user_id, user.name);
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2014
2122
|
onFileCreated: (parentId, type, entity) => {
|
|
2015
2123
|
const parent = this.folderById(parentId);
|
|
2016
2124
|
if (!parent || type === "outputs") {
|
|
@@ -2075,12 +2183,121 @@ var RemoteProject = class {
|
|
|
2075
2183
|
onFileChanged: (update) => {
|
|
2076
2184
|
const path = this.idToPath.get(update.doc);
|
|
2077
2185
|
if (path) {
|
|
2078
|
-
this.emitChange(path);
|
|
2186
|
+
this.emitChange(path, void 0, this.actor(update.meta?.user_id));
|
|
2079
2187
|
}
|
|
2080
2188
|
}
|
|
2081
2189
|
});
|
|
2082
2190
|
}
|
|
2083
2191
|
};
|
|
2192
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2193
|
+
function decode(content) {
|
|
2194
|
+
if (content === void 0) {
|
|
2195
|
+
return "";
|
|
2196
|
+
}
|
|
2197
|
+
if (content.includes(0)) {
|
|
2198
|
+
return void 0;
|
|
2199
|
+
}
|
|
2200
|
+
try {
|
|
2201
|
+
return decoder.decode(content);
|
|
2202
|
+
} catch {
|
|
2203
|
+
return void 0;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
function lineStarts(content) {
|
|
2207
|
+
const starts = [0];
|
|
2208
|
+
for (let index = 0; index < content.length; index++) {
|
|
2209
|
+
if (content[index] === "\n" && index + 1 < content.length) {
|
|
2210
|
+
starts.push(index + 1);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
return starts;
|
|
2214
|
+
}
|
|
2215
|
+
function lineAt(starts, offset) {
|
|
2216
|
+
let low = 0;
|
|
2217
|
+
let high = starts.length;
|
|
2218
|
+
while (low < high) {
|
|
2219
|
+
const middle = Math.floor((low + high) / 2);
|
|
2220
|
+
if (starts[middle] <= offset) {
|
|
2221
|
+
low = middle + 1;
|
|
2222
|
+
} else {
|
|
2223
|
+
high = middle;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return Math.max(1, low);
|
|
2227
|
+
}
|
|
2228
|
+
function toLineRange(content, start, end) {
|
|
2229
|
+
const starts = lineStarts(content);
|
|
2230
|
+
const safeStart = Math.min(start, Math.max(0, content.length - 1));
|
|
2231
|
+
const safeEnd = Math.min(Math.max(start, end - 1), Math.max(0, content.length - 1));
|
|
2232
|
+
return { start: lineAt(starts, safeStart), end: lineAt(starts, safeEnd) };
|
|
2233
|
+
}
|
|
2234
|
+
function mergeRanges(ranges) {
|
|
2235
|
+
const merged = [];
|
|
2236
|
+
for (const range of ranges.sort((a, b) => a.start - b.start)) {
|
|
2237
|
+
const previous = merged.at(-1);
|
|
2238
|
+
if (previous && range.start <= previous.end + 1) {
|
|
2239
|
+
previous.end = Math.max(previous.end, range.end);
|
|
2240
|
+
} else {
|
|
2241
|
+
merged.push({ ...range });
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return merged;
|
|
2245
|
+
}
|
|
2246
|
+
function formatRanges(ranges) {
|
|
2247
|
+
const shown = ranges.slice(0, 3).map((range) => range.start === range.end ? String(range.start) : `${range.start}-${range.end}`);
|
|
2248
|
+
return `${shown.join(", ")}${ranges.length > shown.length ? ` (+${ranges.length - shown.length} more)` : ""}`;
|
|
2249
|
+
}
|
|
2250
|
+
function describeContentChange(before, after) {
|
|
2251
|
+
if (before && after && Buffer.from(before).equals(Buffer.from(after))) {
|
|
2252
|
+
return void 0;
|
|
2253
|
+
}
|
|
2254
|
+
const oldText = decode(before);
|
|
2255
|
+
const newText = decode(after);
|
|
2256
|
+
if (oldText === void 0 || newText === void 0) {
|
|
2257
|
+
return `binary bytes ${before?.byteLength ?? 0} -> ${after?.byteLength ?? 0}`;
|
|
2258
|
+
}
|
|
2259
|
+
if (oldText.length === 0 && newText.length === 0) {
|
|
2260
|
+
return void 0;
|
|
2261
|
+
}
|
|
2262
|
+
if (oldText.length === 0) {
|
|
2263
|
+
const count = lineStarts(newText).length;
|
|
2264
|
+
return `added lines ${count === 1 ? "1" : `1-${count}`}`;
|
|
2265
|
+
}
|
|
2266
|
+
if (newText.length === 0) {
|
|
2267
|
+
const count = lineStarts(oldText).length;
|
|
2268
|
+
return `removed lines ${count === 1 ? "1" : `1-${count}`}`;
|
|
2269
|
+
}
|
|
2270
|
+
const changes = [];
|
|
2271
|
+
let oldOffset = 0;
|
|
2272
|
+
let newOffset = 0;
|
|
2273
|
+
let current;
|
|
2274
|
+
const finish = () => {
|
|
2275
|
+
if (current) {
|
|
2276
|
+
changes.push(current);
|
|
2277
|
+
current = void 0;
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
for (const [operation, value] of new DiffMatchPatch2().diff_main(oldText, newText)) {
|
|
2281
|
+
if (operation === 0) {
|
|
2282
|
+
finish();
|
|
2283
|
+
oldOffset += value.length;
|
|
2284
|
+
newOffset += value.length;
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
current ??= { oldStart: oldOffset, oldEnd: oldOffset, newStart: newOffset, newEnd: newOffset };
|
|
2288
|
+
if (operation === -1) {
|
|
2289
|
+
oldOffset += value.length;
|
|
2290
|
+
}
|
|
2291
|
+
if (operation === 1) {
|
|
2292
|
+
newOffset += value.length;
|
|
2293
|
+
}
|
|
2294
|
+
current.oldEnd = oldOffset;
|
|
2295
|
+
current.newEnd = newOffset;
|
|
2296
|
+
}
|
|
2297
|
+
finish();
|
|
2298
|
+
const ranges = mergeRanges(changes.map((change) => change.newEnd > change.newStart ? toLineRange(newText, change.newStart, change.newEnd) : toLineRange(oldText, change.oldStart, change.oldEnd)));
|
|
2299
|
+
return ranges.length === 0 ? void 0 : `lines ${formatRanges(ranges)}`;
|
|
2300
|
+
}
|
|
2084
2301
|
var textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
2085
2302
|
var textEncoder = new TextEncoder();
|
|
2086
2303
|
function decodeText(content) {
|
|
@@ -2120,32 +2337,19 @@ function mergeText(baseContent, localContent, remoteContent) {
|
|
|
2120
2337
|
content: textEncoder.encode(regions.flatMap((region) => region.ok ?? []).join(""))
|
|
2121
2338
|
};
|
|
2122
2339
|
}
|
|
2123
|
-
var SYNC_STATE_SCHEMA_VERSION =
|
|
2340
|
+
var SYNC_STATE_SCHEMA_VERSION = 1;
|
|
2124
2341
|
function sha256(content) {
|
|
2125
2342
|
return createHash2("sha256").update(content).digest("hex");
|
|
2126
2343
|
}
|
|
2127
|
-
function
|
|
2128
|
-
try {
|
|
2129
|
-
const state = JSON.parse(serialized);
|
|
2130
|
-
if (state.schemaVersion !== 1 && state.schemaVersion !== SYNC_STATE_SCHEMA_VERSION || state.projectUri !== projectUri || !Number.isInteger(state.remoteVersion) || typeof state.files !== "object" || state.files === null) {
|
|
2131
|
-
return void 0;
|
|
2132
|
-
}
|
|
2133
|
-
return {
|
|
2134
|
-
...state,
|
|
2135
|
-
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
2136
|
-
textBases: typeof state.textBases === "object" && state.textBases !== null ? state.textBases : {}
|
|
2137
|
-
};
|
|
2138
|
-
} catch {
|
|
2139
|
-
return void 0;
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
function createSyncState(projectUri, remoteVersion) {
|
|
2344
|
+
function createSyncState(projectUri, remoteVersion, initialized = true) {
|
|
2143
2345
|
return {
|
|
2144
2346
|
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
2145
2347
|
projectUri,
|
|
2146
2348
|
remoteVersion,
|
|
2349
|
+
initialized,
|
|
2147
2350
|
files: {},
|
|
2148
|
-
textBases: {}
|
|
2351
|
+
textBases: {},
|
|
2352
|
+
conflicts: {}
|
|
2149
2353
|
};
|
|
2150
2354
|
}
|
|
2151
2355
|
function updateSyncCheckpoint(state, path, content) {
|
|
@@ -2158,10 +2362,12 @@ function updateSyncCheckpoint(state, path, content) {
|
|
|
2158
2362
|
}
|
|
2159
2363
|
}
|
|
2160
2364
|
function removeSyncCheckpoint(state, path) {
|
|
2161
|
-
|
|
2365
|
+
const checkpointPaths = /* @__PURE__ */ new Set([...Object.keys(state.files), ...Object.keys(state.conflicts)]);
|
|
2366
|
+
for (const checkpointPath of checkpointPaths) {
|
|
2162
2367
|
if (checkpointPath === path || checkpointPath.startsWith(`${path}/`)) {
|
|
2163
2368
|
delete state.files[checkpointPath];
|
|
2164
2369
|
delete state.textBases[checkpointPath];
|
|
2370
|
+
delete state.conflicts[checkpointPath];
|
|
2165
2371
|
}
|
|
2166
2372
|
}
|
|
2167
2373
|
}
|
|
@@ -2184,41 +2390,54 @@ var SyncEngine = class {
|
|
|
2184
2390
|
queue = Promise.resolve();
|
|
2185
2391
|
conflicts = /* @__PURE__ */ new Set();
|
|
2186
2392
|
stopLocalWatch;
|
|
2393
|
+
stopRemoteChange;
|
|
2394
|
+
stopRemoteReconnect;
|
|
2187
2395
|
eventTimers = /* @__PURE__ */ new Map();
|
|
2396
|
+
stopped = false;
|
|
2188
2397
|
async start() {
|
|
2398
|
+
this.stopped = false;
|
|
2189
2399
|
this.state = await this.options.stateStore.load();
|
|
2190
2400
|
const currentVersion = await this.options.remote.getCurrentVersion();
|
|
2191
2401
|
const localFiles = await this.options.local.listFiles();
|
|
2192
2402
|
const remoteFiles = this.remoteFilePaths();
|
|
2193
|
-
if (!this.state) {
|
|
2194
|
-
this.
|
|
2195
|
-
if (localFiles.length !== 0 && remoteFiles.length !== 0) {
|
|
2196
|
-
throw new Error("Synchronization paused: both the local and Overleaf projects contain files, but no common checkpoint exists.");
|
|
2197
|
-
}
|
|
2198
|
-
const source = localFiles.length === 0 ? "remote" : "local";
|
|
2199
|
-
for (const path of source === "remote" ? remoteFiles : localFiles) {
|
|
2200
|
-
await this.reconcilePath(path, source);
|
|
2201
|
-
}
|
|
2202
|
-
this.state.remoteVersion = currentVersion;
|
|
2203
|
-
await this.options.stateStore.save(this.state);
|
|
2403
|
+
if (!this.state || !this.state.initialized) {
|
|
2404
|
+
await this.initializeState(currentVersion, localFiles, remoteFiles);
|
|
2204
2405
|
} else {
|
|
2406
|
+
this.restoreConflicts();
|
|
2205
2407
|
await this.startupReconcile(currentVersion, localFiles, remoteFiles);
|
|
2206
2408
|
}
|
|
2207
2409
|
this.stopLocalWatch = await this.options.local.watch((path) => this.schedule(path, "local"));
|
|
2208
|
-
this.options.remote.onChange((change) => {
|
|
2410
|
+
this.stopRemoteChange = this.options.remote.onChange((change) => {
|
|
2411
|
+
if (this.stopped) {
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2209
2414
|
if (change.previousPath) {
|
|
2210
|
-
this.schedule(change.previousPath, "remote");
|
|
2415
|
+
this.schedule(change.previousPath, "remote", change.actor);
|
|
2416
|
+
}
|
|
2417
|
+
this.schedule(change.path, "remote", change.actor);
|
|
2418
|
+
});
|
|
2419
|
+
this.stopRemoteReconnect = this.options.remote.onReconnect?.(() => {
|
|
2420
|
+
if (this.stopped) {
|
|
2421
|
+
return;
|
|
2211
2422
|
}
|
|
2212
|
-
this.
|
|
2423
|
+
this.queue = this.queue.then(() => this.reconcileAfterReconnect()).catch((error2) => {
|
|
2424
|
+
this.options.log(`Failed to reconcile after reconnecting: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
2425
|
+
});
|
|
2213
2426
|
});
|
|
2214
2427
|
this.options.log(`Synchronization active at Overleaf version ${this.state.remoteVersion}.`);
|
|
2215
2428
|
}
|
|
2216
2429
|
async stop() {
|
|
2217
|
-
|
|
2218
|
-
|
|
2430
|
+
this.stopped = true;
|
|
2431
|
+
this.stopRemoteChange?.();
|
|
2432
|
+
this.stopRemoteChange = void 0;
|
|
2433
|
+
this.stopRemoteReconnect?.();
|
|
2434
|
+
this.stopRemoteReconnect = void 0;
|
|
2435
|
+
for (const pending of this.eventTimers.values()) {
|
|
2436
|
+
clearTimeout(pending.timer);
|
|
2219
2437
|
}
|
|
2220
2438
|
this.eventTimers.clear();
|
|
2221
2439
|
await this.stopLocalWatch?.();
|
|
2440
|
+
this.stopLocalWatch = void 0;
|
|
2222
2441
|
await this.queue;
|
|
2223
2442
|
}
|
|
2224
2443
|
getConflicts() {
|
|
@@ -2254,15 +2473,95 @@ var SyncEngine = class {
|
|
|
2254
2473
|
await this.options.local.remove(normalized);
|
|
2255
2474
|
removeSyncCheckpoint(this.state, normalized);
|
|
2256
2475
|
}
|
|
2257
|
-
await this.options.stateStore.save(this.state);
|
|
2258
2476
|
this.conflicts.delete(normalized);
|
|
2477
|
+
delete this.state.conflicts[normalized];
|
|
2478
|
+
await this.options.stateStore.save(this.state);
|
|
2259
2479
|
this.options.log(`Resolved ${normalized} using ${winner === "local" ? "local" : "Overleaf"} content.`);
|
|
2260
2480
|
});
|
|
2261
2481
|
}
|
|
2262
2482
|
async retry(path) {
|
|
2263
2483
|
const normalized = normalizePath2(path);
|
|
2484
|
+
const reason = this.state.conflicts[normalized];
|
|
2264
2485
|
this.conflicts.delete(normalized);
|
|
2265
|
-
|
|
2486
|
+
delete this.state.conflicts[normalized];
|
|
2487
|
+
try {
|
|
2488
|
+
await this.enqueue(() => this.reconcilePath(normalized, "local"));
|
|
2489
|
+
} catch (error2) {
|
|
2490
|
+
if (reason !== void 0) {
|
|
2491
|
+
this.conflicts.add(normalized);
|
|
2492
|
+
this.state.conflicts[normalized] = reason;
|
|
2493
|
+
await this.options.stateStore.save(this.state);
|
|
2494
|
+
}
|
|
2495
|
+
throw error2;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
async initializeState(currentVersion, localFiles, remoteFiles) {
|
|
2499
|
+
if (!this.state) {
|
|
2500
|
+
this.state = createSyncState(this.options.projectUri, currentVersion, false);
|
|
2501
|
+
await this.options.stateStore.save(this.state);
|
|
2502
|
+
}
|
|
2503
|
+
this.restoreConflicts();
|
|
2504
|
+
const paths = /* @__PURE__ */ new Set([...localFiles, ...remoteFiles, ...Object.keys(this.state.files)]);
|
|
2505
|
+
for (const path of [...paths].sort()) {
|
|
2506
|
+
const normalized = normalizePath2(path);
|
|
2507
|
+
if (this.conflicts.has(normalized)) {
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
if (this.state.files[normalized] !== void 0) {
|
|
2511
|
+
await this.reconcilePath(normalized, "local");
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
await this.initializePath(normalized);
|
|
2515
|
+
}
|
|
2516
|
+
this.state.remoteVersion = currentVersion;
|
|
2517
|
+
this.state.initialized = true;
|
|
2518
|
+
await this.options.stateStore.save(this.state);
|
|
2519
|
+
if (this.conflicts.size !== 0) {
|
|
2520
|
+
this.options.log(`Synchronization remains active; ${this.conflicts.size} conflicting path(s) are paused.`);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
async initializePath(path) {
|
|
2524
|
+
const remoteEntry = this.options.remote.entry(path);
|
|
2525
|
+
if (remoteEntry?.type === "folder") {
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
const localContent = await this.options.local.read(path);
|
|
2529
|
+
const remoteContent = isRemoteFile(remoteEntry) ? await this.options.remote.read(path) : void 0;
|
|
2530
|
+
const localHash = localContent && sha256(localContent);
|
|
2531
|
+
const remoteHash = remoteContent && sha256(remoteContent);
|
|
2532
|
+
if (localHash === remoteHash) {
|
|
2533
|
+
if (localContent) {
|
|
2534
|
+
updateSyncCheckpoint(this.state, path, localContent);
|
|
2535
|
+
await this.options.stateStore.save(this.state);
|
|
2536
|
+
}
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (!localContent && remoteContent) {
|
|
2540
|
+
await this.options.local.write(path, remoteContent);
|
|
2541
|
+
updateSyncCheckpoint(this.state, path, remoteContent);
|
|
2542
|
+
await this.options.stateStore.save(this.state);
|
|
2543
|
+
this.logChange("pull", "add", path, void 0, remoteContent);
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
await this.recordConflict(
|
|
2547
|
+
path,
|
|
2548
|
+
remoteContent ? "Local and Overleaf content differ, but no common checkpoint exists." : "The local path has no matching Overleaf file and no common checkpoint exists."
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
restoreConflicts() {
|
|
2552
|
+
for (const [path, reason] of Object.entries(this.state.conflicts)) {
|
|
2553
|
+
if (this.conflicts.has(path)) {
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
this.conflicts.add(path);
|
|
2557
|
+
this.options.onConflict(path, reason);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
async recordConflict(path, reason) {
|
|
2561
|
+
this.conflicts.add(path);
|
|
2562
|
+
this.state.conflicts[path] = reason;
|
|
2563
|
+
await this.options.stateStore.save(this.state);
|
|
2564
|
+
this.options.onConflict(path, reason);
|
|
2266
2565
|
}
|
|
2267
2566
|
async startupReconcile(currentVersion, localFiles, remoteFiles) {
|
|
2268
2567
|
const state = this.state;
|
|
@@ -2291,6 +2590,11 @@ var SyncEngine = class {
|
|
|
2291
2590
|
}
|
|
2292
2591
|
const candidates = new Set(localChanged);
|
|
2293
2592
|
const knownFiles = /* @__PURE__ */ new Set([...Object.keys(state.files), ...localFiles, ...remoteFiles]);
|
|
2593
|
+
for (const path of [...localFiles, ...remoteFiles]) {
|
|
2594
|
+
if (state.files[path] === void 0) {
|
|
2595
|
+
candidates.add(path);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2294
2598
|
for (const change of remoteDiff.diff) {
|
|
2295
2599
|
const paths = [change.pathname, change.newPathname].filter((path) => Boolean(path));
|
|
2296
2600
|
for (const changedPath of paths.map(normalizePath2)) {
|
|
@@ -2324,30 +2628,61 @@ var SyncEngine = class {
|
|
|
2324
2628
|
this.options.log(`Synchronization remains active; ${this.conflicts.size} conflicting path(s) are paused.`);
|
|
2325
2629
|
}
|
|
2326
2630
|
}
|
|
2327
|
-
|
|
2631
|
+
async reconcileAfterReconnect() {
|
|
2632
|
+
if (this.stopped) {
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
const currentVersion = await this.options.remote.getCurrentVersion();
|
|
2636
|
+
await this.startupReconcile(
|
|
2637
|
+
currentVersion,
|
|
2638
|
+
await this.options.local.listFiles(),
|
|
2639
|
+
this.remoteFilePaths()
|
|
2640
|
+
);
|
|
2641
|
+
this.options.log(`Realtime connection restored at Overleaf version ${currentVersion}.`);
|
|
2642
|
+
}
|
|
2643
|
+
schedule(path, source, actor) {
|
|
2328
2644
|
const normalized = normalizePath2(path);
|
|
2329
|
-
if (this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2645
|
+
if (this.stopped || this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2330
2646
|
return;
|
|
2331
2647
|
}
|
|
2332
2648
|
const previous = this.eventTimers.get(normalized);
|
|
2333
2649
|
if (previous) {
|
|
2334
|
-
clearTimeout(previous);
|
|
2650
|
+
clearTimeout(previous.timer);
|
|
2335
2651
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2652
|
+
const actors = previous?.actors ?? /* @__PURE__ */ new Set();
|
|
2653
|
+
let unknownRemoteActor = previous?.unknownRemoteActor ?? false;
|
|
2654
|
+
if (source === "remote") {
|
|
2655
|
+
if (actor) {
|
|
2656
|
+
actors.add(actor);
|
|
2657
|
+
} else {
|
|
2658
|
+
unknownRemoteActor = true;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
const pending = {
|
|
2662
|
+
source: previous?.source === "local" || source === "local" ? "local" : "remote",
|
|
2663
|
+
actors,
|
|
2664
|
+
unknownRemoteActor,
|
|
2665
|
+
timer: setTimeout(() => {
|
|
2666
|
+
this.eventTimers.delete(normalized);
|
|
2667
|
+
if (this.stopped) {
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
const remoteActor = pending.unknownRemoteActor ? void 0 : pending.actors.size === 1 ? [...pending.actors][0] : pending.actors.size > 1 ? "multiple Overleaf collaborators" : void 0;
|
|
2671
|
+
this.queue = this.queue.then(() => this.reconcilePath(normalized, pending.source, remoteActor)).catch((error2) => {
|
|
2672
|
+
this.options.log(`Failed to synchronize ${normalized}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
2673
|
+
});
|
|
2674
|
+
}, 250)
|
|
2675
|
+
};
|
|
2676
|
+
this.eventTimers.set(normalized, pending);
|
|
2342
2677
|
}
|
|
2343
2678
|
enqueue(operation) {
|
|
2344
2679
|
const result = this.queue.then(operation);
|
|
2345
2680
|
this.queue = result.then(() => void 0, () => void 0);
|
|
2346
2681
|
return result;
|
|
2347
2682
|
}
|
|
2348
|
-
async reconcilePath(path, source) {
|
|
2683
|
+
async reconcilePath(path, source, actor) {
|
|
2349
2684
|
const normalized = normalizePath2(path);
|
|
2350
|
-
if (this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2685
|
+
if (this.stopped || this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2351
2686
|
return;
|
|
2352
2687
|
}
|
|
2353
2688
|
const remoteEntry = this.options.remote.entry(normalized);
|
|
@@ -2357,38 +2692,43 @@ var SyncEngine = class {
|
|
|
2357
2692
|
const localContent = await this.options.local.read(normalized);
|
|
2358
2693
|
const remoteContent = isRemoteFile(remoteEntry) ? await this.options.remote.read(normalized) : void 0;
|
|
2359
2694
|
const baseHash = this.state.files[normalized];
|
|
2695
|
+
const baseContent = readTextBase(this.state, normalized);
|
|
2360
2696
|
const localHash = localContent && sha256(localContent);
|
|
2361
2697
|
const remoteHash = remoteContent && sha256(remoteContent);
|
|
2362
2698
|
if (localHash === remoteHash) {
|
|
2363
|
-
if (
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2699
|
+
if (localHash !== baseHash) {
|
|
2700
|
+
if (localContent) {
|
|
2701
|
+
updateSyncCheckpoint(this.state, normalized, localContent);
|
|
2702
|
+
} else {
|
|
2703
|
+
removeSyncCheckpoint(this.state, normalized);
|
|
2704
|
+
}
|
|
2705
|
+
await this.options.stateStore.save(this.state);
|
|
2367
2706
|
}
|
|
2368
|
-
await this.options.stateStore.save(this.state);
|
|
2369
2707
|
return;
|
|
2370
2708
|
}
|
|
2371
2709
|
const localChanged = localHash !== baseHash;
|
|
2372
2710
|
const remoteChanged = remoteHash !== baseHash;
|
|
2373
2711
|
if (localChanged && remoteChanged) {
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
const merged = mergeText(base, localContent, remoteContent);
|
|
2712
|
+
if (baseContent && localContent && remoteContent) {
|
|
2713
|
+
const merged = mergeText(baseContent, localContent, remoteContent);
|
|
2377
2714
|
if (merged.status === "merged" && merged.content) {
|
|
2378
2715
|
await this.options.local.write(normalized, merged.content);
|
|
2379
2716
|
await this.options.remote.write(normalized, merged.content);
|
|
2380
2717
|
updateSyncCheckpoint(this.state, normalized, merged.content);
|
|
2381
2718
|
await this.options.stateStore.save(this.state);
|
|
2382
|
-
this.logChange("merge", "update", normalized);
|
|
2719
|
+
this.logChange("merge", "update", normalized, baseContent, merged.content, actor);
|
|
2383
2720
|
return;
|
|
2384
2721
|
}
|
|
2385
2722
|
}
|
|
2386
|
-
this.conflicts.add(normalized);
|
|
2387
2723
|
const reason = "Both local and Overleaf content changed since the last synchronized checkpoint.";
|
|
2388
|
-
this.
|
|
2724
|
+
await this.recordConflict(normalized, reason);
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (source === "remote" && localChanged && !remoteChanged) {
|
|
2389
2728
|
return;
|
|
2390
2729
|
}
|
|
2391
2730
|
if (localChanged || !remoteChanged && source === "local") {
|
|
2731
|
+
let pushedContent = localContent;
|
|
2392
2732
|
if (!localContent) {
|
|
2393
2733
|
if (remoteEntry) {
|
|
2394
2734
|
await this.options.remote.remove(normalized);
|
|
@@ -2400,13 +2740,21 @@ var SyncEngine = class {
|
|
|
2400
2740
|
if (sha256(synchronized) !== sha256(localContent)) {
|
|
2401
2741
|
await this.options.local.write(normalized, synchronized);
|
|
2402
2742
|
}
|
|
2743
|
+
pushedContent = synchronized;
|
|
2403
2744
|
updateSyncCheckpoint(this.state, normalized, synchronized);
|
|
2404
2745
|
}
|
|
2405
2746
|
await this.options.stateStore.save(this.state);
|
|
2406
2747
|
if (!localContent && remoteEntry) {
|
|
2407
|
-
this.logChange("push", "delete", normalized);
|
|
2748
|
+
this.logChange("push", "delete", normalized, baseContent ?? remoteContent, void 0, actor);
|
|
2408
2749
|
} else if (localContent) {
|
|
2409
|
-
this.logChange(
|
|
2750
|
+
this.logChange(
|
|
2751
|
+
"push",
|
|
2752
|
+
remoteEntry ? "update" : "add",
|
|
2753
|
+
normalized,
|
|
2754
|
+
baseContent ?? remoteContent,
|
|
2755
|
+
pushedContent,
|
|
2756
|
+
actor
|
|
2757
|
+
);
|
|
2410
2758
|
}
|
|
2411
2759
|
return;
|
|
2412
2760
|
}
|
|
@@ -2421,13 +2769,26 @@ var SyncEngine = class {
|
|
|
2421
2769
|
}
|
|
2422
2770
|
await this.options.stateStore.save(this.state);
|
|
2423
2771
|
if (!remoteContent && localContent) {
|
|
2424
|
-
this.logChange("pull", "delete", normalized);
|
|
2772
|
+
this.logChange("pull", "delete", normalized, baseContent ?? localContent, void 0, actor);
|
|
2425
2773
|
} else if (remoteContent) {
|
|
2426
|
-
this.logChange(
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2774
|
+
this.logChange(
|
|
2775
|
+
"pull",
|
|
2776
|
+
localContent ? "update" : "add",
|
|
2777
|
+
normalized,
|
|
2778
|
+
baseContent ?? localContent,
|
|
2779
|
+
remoteContent,
|
|
2780
|
+
actor
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
logChange(direction, operation, path, before, after, actor) {
|
|
2785
|
+
const summary = describeContentChange(before, after);
|
|
2786
|
+
const source = direction === "push" ? "local filesystem" : direction === "pull" ? actor ?? "Overleaf" : `local filesystem + ${actor ?? "Overleaf"}`;
|
|
2787
|
+
this.options.log([
|
|
2788
|
+
`[${direction}] ${operation} ${JSON.stringify(path)}`,
|
|
2789
|
+
summary,
|
|
2790
|
+
`source=${source}`
|
|
2791
|
+
].filter(Boolean).join("; "));
|
|
2431
2792
|
}
|
|
2432
2793
|
remoteFilePaths() {
|
|
2433
2794
|
return this.options.remote.listEntries().filter((entry) => isRemoteFile(entry) && !this.shouldIgnore(entry.path)).map((entry) => entry.path);
|
|
@@ -2436,39 +2797,325 @@ var SyncEngine = class {
|
|
|
2436
2797
|
return this.options.ignore(path) || path.split("/").some((part) => part.startsWith("."));
|
|
2437
2798
|
}
|
|
2438
2799
|
};
|
|
2800
|
+
var HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
2801
|
+
var encoder = new TextEncoder();
|
|
2802
|
+
function json(value) {
|
|
2803
|
+
return `${JSON.stringify(value, null, 2)}
|
|
2804
|
+
`;
|
|
2805
|
+
}
|
|
2806
|
+
function pathKey(path) {
|
|
2807
|
+
return sha256(encoder.encode(path));
|
|
2808
|
+
}
|
|
2809
|
+
async function readJson(path) {
|
|
2810
|
+
return JSON.parse(await readFile4(path, "utf8"));
|
|
2811
|
+
}
|
|
2812
|
+
async function listFiles(directory) {
|
|
2813
|
+
const result = [];
|
|
2814
|
+
const visit = async (current) => {
|
|
2815
|
+
let entries;
|
|
2816
|
+
try {
|
|
2817
|
+
entries = await readdir2(current, { withFileTypes: true });
|
|
2818
|
+
} catch (error2) {
|
|
2819
|
+
if (error2.code === "ENOENT") {
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
throw error2;
|
|
2823
|
+
}
|
|
2824
|
+
for (const entry of entries) {
|
|
2825
|
+
const path = join2(current, entry.name);
|
|
2826
|
+
if (entry.isDirectory()) {
|
|
2827
|
+
await visit(path);
|
|
2828
|
+
} else if (entry.isFile()) {
|
|
2829
|
+
result.push(path);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
await visit(directory);
|
|
2834
|
+
return result;
|
|
2835
|
+
}
|
|
2836
|
+
async function atomicWrite(path, content) {
|
|
2837
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
2838
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
2839
|
+
try {
|
|
2840
|
+
await writeFile3(temporary, content);
|
|
2841
|
+
await rename(temporary, path);
|
|
2842
|
+
} finally {
|
|
2843
|
+
await rm2(temporary, { force: true });
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
function parseFormat(value, projectUri) {
|
|
2847
|
+
if (typeof value !== "object" || value === null) {
|
|
2848
|
+
return false;
|
|
2849
|
+
}
|
|
2850
|
+
const format = value;
|
|
2851
|
+
return format.schemaVersion === SYNC_STATE_SCHEMA_VERSION && format.projectUri === projectUri;
|
|
2852
|
+
}
|
|
2853
|
+
function parseCursor(value) {
|
|
2854
|
+
if (typeof value !== "object" || value === null) {
|
|
2855
|
+
return false;
|
|
2856
|
+
}
|
|
2857
|
+
const cursor = value;
|
|
2858
|
+
return Number.isInteger(cursor.remoteVersion) && cursor.remoteVersion >= 0 && typeof cursor.initialized === "boolean";
|
|
2859
|
+
}
|
|
2860
|
+
function parsePathRecord(value) {
|
|
2861
|
+
if (typeof value !== "object" || value === null) {
|
|
2862
|
+
return false;
|
|
2863
|
+
}
|
|
2864
|
+
const record = value;
|
|
2865
|
+
if (typeof record.path !== "string" || !record.path.startsWith("/")) {
|
|
2866
|
+
return false;
|
|
2867
|
+
}
|
|
2868
|
+
if (record.hash !== void 0 && (typeof record.hash !== "string" || !HASH_PATTERN.test(record.hash))) {
|
|
2869
|
+
return false;
|
|
2870
|
+
}
|
|
2871
|
+
if (record.baseHash !== void 0 && (typeof record.baseHash !== "string" || !HASH_PATTERN.test(record.baseHash) || record.hash !== record.baseHash)) {
|
|
2872
|
+
return false;
|
|
2873
|
+
}
|
|
2874
|
+
if (record.conflict !== void 0 && typeof record.conflict !== "string") {
|
|
2875
|
+
return false;
|
|
2876
|
+
}
|
|
2877
|
+
return record.hash !== void 0 || record.conflict !== void 0;
|
|
2878
|
+
}
|
|
2879
|
+
function createRecord(state, path) {
|
|
2880
|
+
const hash = state.files[path];
|
|
2881
|
+
const textBase = state.textBases[path];
|
|
2882
|
+
const conflict = state.conflicts[path];
|
|
2883
|
+
if (hash === void 0 && conflict === void 0) {
|
|
2884
|
+
return void 0;
|
|
2885
|
+
}
|
|
2886
|
+
return {
|
|
2887
|
+
path,
|
|
2888
|
+
...hash === void 0 ? {} : { hash },
|
|
2889
|
+
...textBase === void 0 ? {} : { baseHash: hash },
|
|
2890
|
+
...conflict === void 0 ? {} : { conflict }
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2439
2893
|
var FileSyncStateStore = class {
|
|
2440
2894
|
constructor(directory, projectUri) {
|
|
2441
2895
|
this.projectUri = projectUri;
|
|
2442
|
-
|
|
2896
|
+
const overleafDirectory = resolve4(directory, ".overleaf");
|
|
2897
|
+
this.legacyPath = join2(overleafDirectory, "sync-state.json");
|
|
2898
|
+
this.syncDirectory = join2(overleafDirectory, "sync");
|
|
2899
|
+
this.formatPath = join2(this.syncDirectory, "format.json");
|
|
2900
|
+
this.cursorPath = join2(this.syncDirectory, "cursor.json");
|
|
2901
|
+
this.pathsDirectory = join2(this.syncDirectory, "paths");
|
|
2902
|
+
this.basesDirectory = join2(this.syncDirectory, "bases");
|
|
2443
2903
|
}
|
|
2444
2904
|
projectUri;
|
|
2445
|
-
|
|
2446
|
-
|
|
2905
|
+
legacyPath;
|
|
2906
|
+
syncDirectory;
|
|
2907
|
+
formatPath;
|
|
2908
|
+
cursorPath;
|
|
2909
|
+
pathsDirectory;
|
|
2910
|
+
basesDirectory;
|
|
2911
|
+
persistedRecords = /* @__PURE__ */ new Map();
|
|
2912
|
+
persistedCursor;
|
|
2913
|
+
invalidRecordPaths = /* @__PURE__ */ new Set();
|
|
2914
|
+
initialized = false;
|
|
2447
2915
|
async load() {
|
|
2916
|
+
this.invalidRecordPaths.clear();
|
|
2448
2917
|
try {
|
|
2449
|
-
const
|
|
2450
|
-
const
|
|
2451
|
-
if (
|
|
2452
|
-
|
|
2453
|
-
|
|
2918
|
+
const format = await readJson(this.formatPath);
|
|
2919
|
+
const cursor = await readJson(this.cursorPath);
|
|
2920
|
+
if (!parseFormat(format, this.projectUri) || !parseCursor(cursor)) {
|
|
2921
|
+
return void 0;
|
|
2922
|
+
}
|
|
2923
|
+
const state = {
|
|
2924
|
+
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
2925
|
+
projectUri: this.projectUri,
|
|
2926
|
+
remoteVersion: cursor.remoteVersion,
|
|
2927
|
+
initialized: cursor.initialized,
|
|
2928
|
+
files: {},
|
|
2929
|
+
textBases: {},
|
|
2930
|
+
conflicts: {}
|
|
2931
|
+
};
|
|
2932
|
+
const records = /* @__PURE__ */ new Map();
|
|
2933
|
+
const referencedBases = /* @__PURE__ */ new Set();
|
|
2934
|
+
let stateIncomplete = false;
|
|
2935
|
+
for (const recordPath of await listFiles(this.pathsDirectory)) {
|
|
2936
|
+
const record = await this.loadRecord(recordPath);
|
|
2937
|
+
if (!record) {
|
|
2938
|
+
if (recordPath.endsWith(".json")) {
|
|
2939
|
+
this.invalidRecordPaths.add(recordPath);
|
|
2940
|
+
stateIncomplete = true;
|
|
2941
|
+
} else {
|
|
2942
|
+
await rm2(recordPath, { force: true });
|
|
2943
|
+
}
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
if (record.hash !== void 0) {
|
|
2947
|
+
state.files[record.path] = record.hash;
|
|
2948
|
+
}
|
|
2949
|
+
if (record.baseHash !== void 0) {
|
|
2950
|
+
let textBase;
|
|
2951
|
+
try {
|
|
2952
|
+
textBase = await readFile4(this.basePath(record.baseHash), "utf8");
|
|
2953
|
+
} catch (error2) {
|
|
2954
|
+
if (error2.code === "ENOENT") {
|
|
2955
|
+
this.invalidRecordPaths.add(recordPath);
|
|
2956
|
+
stateIncomplete = true;
|
|
2957
|
+
delete state.files[record.path];
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
throw error2;
|
|
2961
|
+
}
|
|
2962
|
+
if (sha256(encoder.encode(textBase)) !== record.baseHash) {
|
|
2963
|
+
this.invalidRecordPaths.add(recordPath);
|
|
2964
|
+
stateIncomplete = true;
|
|
2965
|
+
delete state.files[record.path];
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
state.textBases[record.path] = textBase;
|
|
2969
|
+
referencedBases.add(record.baseHash);
|
|
2970
|
+
}
|
|
2971
|
+
if (record.conflict !== void 0) {
|
|
2972
|
+
state.conflicts[record.path] = record.conflict;
|
|
2973
|
+
}
|
|
2974
|
+
records.set(record.path, json(record));
|
|
2975
|
+
}
|
|
2976
|
+
if (stateIncomplete) {
|
|
2977
|
+
state.initialized = false;
|
|
2454
2978
|
}
|
|
2979
|
+
await this.removeOrphanBases(referencedBases);
|
|
2980
|
+
this.persistedRecords = records;
|
|
2981
|
+
this.persistedCursor = json(cursor);
|
|
2982
|
+
this.initialized = true;
|
|
2455
2983
|
return state;
|
|
2456
2984
|
} catch (error2) {
|
|
2457
|
-
if (error2.code === "ENOENT") {
|
|
2985
|
+
if (error2.code === "ENOENT" || error2 instanceof SyntaxError) {
|
|
2458
2986
|
return void 0;
|
|
2459
2987
|
}
|
|
2460
2988
|
throw error2;
|
|
2461
2989
|
}
|
|
2462
2990
|
}
|
|
2463
2991
|
async save(state) {
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2992
|
+
if (state.projectUri !== this.projectUri || state.schemaVersion !== SYNC_STATE_SCHEMA_VERSION) {
|
|
2993
|
+
throw new Error("Cannot persist synchronization state for a different project or schema.");
|
|
2994
|
+
}
|
|
2995
|
+
if (!this.initialized) {
|
|
2996
|
+
await this.initializeStorage();
|
|
2997
|
+
}
|
|
2998
|
+
const paths = /* @__PURE__ */ new Set([
|
|
2999
|
+
...this.persistedRecords.keys(),
|
|
3000
|
+
...Object.keys(state.files),
|
|
3001
|
+
...Object.keys(state.conflicts)
|
|
3002
|
+
]);
|
|
3003
|
+
const obsoleteBases = /* @__PURE__ */ new Set();
|
|
3004
|
+
for (const path of [...paths].sort()) {
|
|
3005
|
+
const record = createRecord(state, path);
|
|
3006
|
+
const serialized = record && json(record);
|
|
3007
|
+
const previous = this.persistedRecords.get(path);
|
|
3008
|
+
if (serialized === previous) {
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
if (previous) {
|
|
3012
|
+
const previousRecord = JSON.parse(previous);
|
|
3013
|
+
if (previousRecord.baseHash) {
|
|
3014
|
+
obsoleteBases.add(previousRecord.baseHash);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
if (!record || !serialized) {
|
|
3018
|
+
await rm2(this.recordPath(path), { force: true });
|
|
3019
|
+
this.persistedRecords.delete(path);
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
if (record.baseHash) {
|
|
3023
|
+
const textBase = state.textBases[path];
|
|
3024
|
+
if (textBase === void 0 || sha256(encoder.encode(textBase)) !== record.baseHash) {
|
|
3025
|
+
throw new Error(`Text checkpoint does not match its content hash: ${path}`);
|
|
3026
|
+
}
|
|
3027
|
+
await this.writeBase(record.baseHash, textBase);
|
|
3028
|
+
}
|
|
3029
|
+
const destination = this.recordPath(path);
|
|
3030
|
+
await atomicWrite(destination, serialized);
|
|
3031
|
+
this.invalidRecordPaths.delete(destination);
|
|
3032
|
+
this.persistedRecords.set(path, serialized);
|
|
3033
|
+
}
|
|
3034
|
+
const cursor = json({ remoteVersion: state.remoteVersion, initialized: state.initialized });
|
|
3035
|
+
if (cursor !== this.persistedCursor) {
|
|
3036
|
+
await atomicWrite(this.cursorPath, cursor);
|
|
3037
|
+
this.persistedCursor = cursor;
|
|
3038
|
+
}
|
|
3039
|
+
if (state.initialized) {
|
|
3040
|
+
await Promise.all([...this.invalidRecordPaths].map((path) => rm2(path, { force: true })));
|
|
3041
|
+
this.invalidRecordPaths.clear();
|
|
3042
|
+
}
|
|
3043
|
+
await rm2(this.legacyPath, { force: true });
|
|
3044
|
+
await this.removeObsoleteBases(state, obsoleteBases);
|
|
3045
|
+
}
|
|
3046
|
+
async initializeStorage() {
|
|
3047
|
+
await rm2(this.syncDirectory, { recursive: true, force: true });
|
|
3048
|
+
await mkdir3(this.pathsDirectory, { recursive: true });
|
|
3049
|
+
await mkdir3(this.basesDirectory, { recursive: true });
|
|
3050
|
+
await atomicWrite(this.formatPath, json({
|
|
3051
|
+
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
3052
|
+
projectUri: this.projectUri
|
|
3053
|
+
}));
|
|
3054
|
+
this.persistedRecords.clear();
|
|
3055
|
+
this.persistedCursor = void 0;
|
|
3056
|
+
this.invalidRecordPaths.clear();
|
|
3057
|
+
this.initialized = true;
|
|
3058
|
+
}
|
|
3059
|
+
recordPath(path) {
|
|
3060
|
+
const key = pathKey(path);
|
|
3061
|
+
return join2(this.pathsDirectory, key.slice(0, 2), `${key}.json`);
|
|
3062
|
+
}
|
|
3063
|
+
async loadRecord(recordPath) {
|
|
3064
|
+
if (!recordPath.endsWith(".json")) {
|
|
3065
|
+
return void 0;
|
|
3066
|
+
}
|
|
3067
|
+
try {
|
|
3068
|
+
const record = await readJson(recordPath);
|
|
3069
|
+
if (!parsePathRecord(record) || recordPath !== this.recordPath(record.path)) {
|
|
3070
|
+
return void 0;
|
|
3071
|
+
}
|
|
3072
|
+
return record;
|
|
3073
|
+
} catch (error2) {
|
|
3074
|
+
if (error2.code === "ENOENT" || error2 instanceof SyntaxError) {
|
|
3075
|
+
return void 0;
|
|
3076
|
+
}
|
|
3077
|
+
throw error2;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
basePath(hash) {
|
|
3081
|
+
return join2(this.basesDirectory, hash.slice(0, 2), `${hash}.txt`);
|
|
3082
|
+
}
|
|
3083
|
+
async writeBase(hash, content) {
|
|
3084
|
+
const path = this.basePath(hash);
|
|
3085
|
+
try {
|
|
3086
|
+
if (sha256(encoder.encode(await readFile4(path, "utf8"))) === hash) {
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
} catch (error2) {
|
|
3090
|
+
if (error2.code !== "ENOENT") {
|
|
3091
|
+
throw error2;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
await atomicWrite(path, content);
|
|
3095
|
+
}
|
|
3096
|
+
async removeObsoleteBases(state, candidates) {
|
|
3097
|
+
if (candidates.size === 0) {
|
|
2467
3098
|
return;
|
|
2468
3099
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
3100
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
3101
|
+
for (const [path, textBase] of Object.entries(state.textBases)) {
|
|
3102
|
+
if (textBase !== void 0 && state.files[path] !== void 0) {
|
|
3103
|
+
referenced.add(state.files[path]);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
for (const hash of candidates) {
|
|
3107
|
+
if (!referenced.has(hash)) {
|
|
3108
|
+
await rm2(this.basePath(hash), { force: true });
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
async removeOrphanBases(referencedHashes) {
|
|
3113
|
+
const referencedPaths = new Set([...referencedHashes].map((hash) => this.basePath(hash)));
|
|
3114
|
+
for (const path of await listFiles(this.basesDirectory)) {
|
|
3115
|
+
if (!referencedPaths.has(path)) {
|
|
3116
|
+
await rm2(path, { force: true });
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
2472
3119
|
}
|
|
2473
3120
|
};
|
|
2474
3121
|
async function startServe(directory = process.cwd(), options = {}) {
|
|
@@ -2534,4 +3181,4 @@ export {
|
|
|
2534
3181
|
RemoteProject,
|
|
2535
3182
|
startServe
|
|
2536
3183
|
};
|
|
2537
|
-
//# sourceMappingURL=chunk-
|
|
3184
|
+
//# sourceMappingURL=chunk-OZ4FLSVP.js.map
|