leafrelay 0.1.6
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/LICENSE +661 -0
- package/README.md +14 -0
- package/README.zh-CN.md +14 -0
- package/dist/chunk-MNOPLM7M.js +2469 -0
- package/dist/chunk-MNOPLM7M.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +77 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +734 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,2469 @@
|
|
|
1
|
+
// ../core/dist/index.js
|
|
2
|
+
import { Blob as Blob2 } from "buffer";
|
|
3
|
+
import mime from "mime";
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
import { v4 as uuidv4 } from "uuid";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { dirname, join, resolve } from "path";
|
|
10
|
+
import { lstat, mkdir as mkdir2, readFile as readFile2, readdir, rm, writeFile as writeFile2 } from "fs/promises";
|
|
11
|
+
import { dirname as dirname2, relative, resolve as resolve2, sep } from "path";
|
|
12
|
+
import { watch } from "chokidar";
|
|
13
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
14
|
+
import { resolve as resolve3 } from "path";
|
|
15
|
+
import { resolve as resolve5 } from "path";
|
|
16
|
+
import { minimatch } from "minimatch";
|
|
17
|
+
import { createHash } from "crypto";
|
|
18
|
+
import { posix } from "path";
|
|
19
|
+
import DiffMatchPatch from "diff-match-patch";
|
|
20
|
+
import { posix as posix2 } from "path";
|
|
21
|
+
import { createHash as createHash2 } from "crypto";
|
|
22
|
+
import { diff3Merge } from "node-diff3";
|
|
23
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
24
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
25
|
+
var PACKET_TYPES = [
|
|
26
|
+
"disconnect",
|
|
27
|
+
"connect",
|
|
28
|
+
"heartbeat",
|
|
29
|
+
"message",
|
|
30
|
+
"json",
|
|
31
|
+
"event",
|
|
32
|
+
"ack",
|
|
33
|
+
"error",
|
|
34
|
+
"noop"
|
|
35
|
+
];
|
|
36
|
+
function encodePacket(packet) {
|
|
37
|
+
const type = PACKET_TYPES.indexOf(packet.type ?? "noop");
|
|
38
|
+
const id = packet.id ?? "";
|
|
39
|
+
let data;
|
|
40
|
+
switch (packet.type) {
|
|
41
|
+
case "event":
|
|
42
|
+
data = JSON.stringify({ name: packet.name, args: packet.args ?? [] });
|
|
43
|
+
break;
|
|
44
|
+
case "ack":
|
|
45
|
+
data = `${packet.ackId ?? ""}${packet.args?.length ? `+${JSON.stringify(packet.args)}` : ""}`;
|
|
46
|
+
break;
|
|
47
|
+
case "message":
|
|
48
|
+
data = String(packet.data ?? "");
|
|
49
|
+
break;
|
|
50
|
+
case "json":
|
|
51
|
+
data = JSON.stringify(packet.data);
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const fields = [String(type), `${id}${packet.ack === "data" ? "+" : ""}`, packet.endpoint ?? ""];
|
|
57
|
+
if (data !== void 0) {
|
|
58
|
+
fields.push(data);
|
|
59
|
+
}
|
|
60
|
+
return fields.join(":");
|
|
61
|
+
}
|
|
62
|
+
function decodePacket(value) {
|
|
63
|
+
const match = value.match(/([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/);
|
|
64
|
+
if (!match) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
const packet = {
|
|
68
|
+
type: PACKET_TYPES[Number(match[1])],
|
|
69
|
+
endpoint: match[4] || ""
|
|
70
|
+
};
|
|
71
|
+
if (match[2]) {
|
|
72
|
+
packet.id = match[2];
|
|
73
|
+
packet.ack = match[3] ? "data" : true;
|
|
74
|
+
}
|
|
75
|
+
const data = match[5] || "";
|
|
76
|
+
switch (packet.type) {
|
|
77
|
+
case "event": {
|
|
78
|
+
const event = JSON.parse(data);
|
|
79
|
+
packet.name = event.name;
|
|
80
|
+
packet.args = event.args ?? [];
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "ack": {
|
|
84
|
+
const ack = data.match(/^([0-9]+)(\+)?([\s\S]*)/);
|
|
85
|
+
if (ack) {
|
|
86
|
+
packet.ackId = ack[1];
|
|
87
|
+
packet.args = ack[3] ? JSON.parse(ack[3]) : [];
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "error": {
|
|
92
|
+
const [reason, advice] = data.split("+");
|
|
93
|
+
packet.reason = ["transport not supported", "client not handshaken", "unauthorized"][Number(reason)] ?? "";
|
|
94
|
+
packet.advice = ["reconnect"][Number(advice)] ?? "";
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "json":
|
|
98
|
+
packet.data = JSON.parse(data);
|
|
99
|
+
break;
|
|
100
|
+
case "message":
|
|
101
|
+
packet.data = data;
|
|
102
|
+
break;
|
|
103
|
+
default:
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
return packet;
|
|
107
|
+
}
|
|
108
|
+
function decodePayload(value) {
|
|
109
|
+
if (!value.startsWith("\uFFFD")) {
|
|
110
|
+
return [decodePacket(value)];
|
|
111
|
+
}
|
|
112
|
+
const packets = [];
|
|
113
|
+
let offset = 1;
|
|
114
|
+
while (offset < value.length) {
|
|
115
|
+
const separator = value.indexOf("\uFFFD", offset);
|
|
116
|
+
if (separator === -1) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const length = Number(value.slice(offset, separator));
|
|
120
|
+
const start = separator + 1;
|
|
121
|
+
packets.push(decodePacket(value.slice(start, start + length)));
|
|
122
|
+
offset = start + length + 1;
|
|
123
|
+
}
|
|
124
|
+
return packets;
|
|
125
|
+
}
|
|
126
|
+
function mergeCookies(current, additions) {
|
|
127
|
+
const cookies = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const value of [current, ...additions.map((cookie) => cookie.split(";", 1)[0])]) {
|
|
129
|
+
for (const part of value.split(";")) {
|
|
130
|
+
const separator = part.indexOf("=");
|
|
131
|
+
if (separator === -1) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
cookies.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [...cookies].map(([name, value]) => `${name}=${value}`).join("; ");
|
|
138
|
+
}
|
|
139
|
+
var OverleafRealtimeSocket = class {
|
|
140
|
+
constructor(url, options) {
|
|
141
|
+
this.options = options;
|
|
142
|
+
this.endpoint = new URL(url);
|
|
143
|
+
this.origin = options.origin;
|
|
144
|
+
this.cookie = options.cookie;
|
|
145
|
+
this.reconnectEnabled = options.reconnect ?? true;
|
|
146
|
+
void this.connect();
|
|
147
|
+
}
|
|
148
|
+
options;
|
|
149
|
+
events = new EventEmitter();
|
|
150
|
+
endpoint;
|
|
151
|
+
origin;
|
|
152
|
+
cookie;
|
|
153
|
+
socket;
|
|
154
|
+
connected = false;
|
|
155
|
+
explicitlyDisconnected = false;
|
|
156
|
+
reconnectEnabled;
|
|
157
|
+
reconnectAttempts = 0;
|
|
158
|
+
reconnectTimer;
|
|
159
|
+
inactivityTimer;
|
|
160
|
+
closeTimeoutMs = 6e4;
|
|
161
|
+
acknowledgementId = 0;
|
|
162
|
+
acknowledgements = /* @__PURE__ */ new Map();
|
|
163
|
+
sendBuffer = [];
|
|
164
|
+
io = {
|
|
165
|
+
reconnect: (enabled) => {
|
|
166
|
+
this.reconnectEnabled = enabled;
|
|
167
|
+
if (!enabled && this.reconnectTimer) {
|
|
168
|
+
clearTimeout(this.reconnectTimer);
|
|
169
|
+
this.reconnectTimer = void 0;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
on(event, handler) {
|
|
174
|
+
this.events.on(event, handler);
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
once(event, handler) {
|
|
178
|
+
this.events.once(event, handler);
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
removeListener(event, handler) {
|
|
182
|
+
this.events.removeListener(event, handler);
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
removeAllListeners(event) {
|
|
186
|
+
this.events.removeAllListeners(event);
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
emit(event, ...args) {
|
|
190
|
+
const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
|
|
191
|
+
const id = callback ? String(++this.acknowledgementId) : void 0;
|
|
192
|
+
if (id && callback) {
|
|
193
|
+
this.acknowledgements.set(id, callback);
|
|
194
|
+
}
|
|
195
|
+
this.send(encodePacket({
|
|
196
|
+
type: "event",
|
|
197
|
+
id,
|
|
198
|
+
ack: callback ? "data" : void 0,
|
|
199
|
+
name: event,
|
|
200
|
+
args
|
|
201
|
+
}));
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
disconnect() {
|
|
205
|
+
this.explicitlyDisconnected = true;
|
|
206
|
+
this.reconnectEnabled = false;
|
|
207
|
+
if (this.reconnectTimer) {
|
|
208
|
+
clearTimeout(this.reconnectTimer);
|
|
209
|
+
}
|
|
210
|
+
if (this.connected) {
|
|
211
|
+
this.send(encodePacket({ type: "disconnect" }));
|
|
212
|
+
}
|
|
213
|
+
this.closeSocket("booted");
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
async connect() {
|
|
217
|
+
try {
|
|
218
|
+
const handshakeUrl = new URL("/socket.io/1/", this.endpoint.origin);
|
|
219
|
+
for (const [name, value] of this.endpoint.searchParams) {
|
|
220
|
+
handshakeUrl.searchParams.set(name, value);
|
|
221
|
+
}
|
|
222
|
+
handshakeUrl.searchParams.set("t", String(Date.now()));
|
|
223
|
+
const response = await fetch(handshakeUrl, {
|
|
224
|
+
headers: { origin: this.origin, cookie: this.cookie },
|
|
225
|
+
redirect: "manual"
|
|
226
|
+
});
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
throw new Error(`Socket.IO handshake failed: HTTP ${response.status} ${await response.text()}`);
|
|
229
|
+
}
|
|
230
|
+
const setCookies = response.headers.getSetCookie?.() ?? [];
|
|
231
|
+
this.cookie = mergeCookies(this.cookie, setCookies);
|
|
232
|
+
const [sessionId, heartbeatTimeout, closeTimeout, transports] = (await response.text()).split(":");
|
|
233
|
+
if (!sessionId || !transports?.split(",").includes("websocket")) {
|
|
234
|
+
throw new Error("Overleaf did not offer a Socket.IO WebSocket transport.");
|
|
235
|
+
}
|
|
236
|
+
this.closeTimeoutMs = Math.max(Number(heartbeatTimeout), Number(closeTimeout), 10) * 1e3;
|
|
237
|
+
const websocketUrl = new URL(`/socket.io/1/websocket/${sessionId}`, this.endpoint.origin);
|
|
238
|
+
websocketUrl.protocol = websocketUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
239
|
+
websocketUrl.search = this.endpoint.search;
|
|
240
|
+
this.socket = new WebSocket(websocketUrl, {
|
|
241
|
+
headers: { origin: this.origin, cookie: this.cookie }
|
|
242
|
+
});
|
|
243
|
+
this.socket.addEventListener("open", () => this.events.emit("connecting", "websocket"));
|
|
244
|
+
this.socket.addEventListener("message", (event) => void this.receive(event.data));
|
|
245
|
+
this.socket.addEventListener("error", (error2) => this.events.emit("error", error2));
|
|
246
|
+
this.socket.addEventListener("close", () => this.handleClose());
|
|
247
|
+
} catch (error2) {
|
|
248
|
+
this.events.emit("connect_failed", error2);
|
|
249
|
+
this.events.emit("error", error2);
|
|
250
|
+
this.scheduleReconnect();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async receive(data) {
|
|
254
|
+
this.resetInactivityTimer();
|
|
255
|
+
const value = typeof data === "string" ? data : data instanceof Blob ? await data.text() : Buffer.from(data).toString("utf8");
|
|
256
|
+
for (const packet of decodePayload(value)) {
|
|
257
|
+
this.handlePacket(packet);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
handlePacket(packet) {
|
|
261
|
+
switch (packet.type) {
|
|
262
|
+
case "connect": {
|
|
263
|
+
const wasReconnecting = this.reconnectAttempts > 0;
|
|
264
|
+
this.connected = true;
|
|
265
|
+
this.events.emit("connect");
|
|
266
|
+
this.flush();
|
|
267
|
+
if (wasReconnecting) {
|
|
268
|
+
this.events.emit("reconnect", "websocket", this.reconnectAttempts);
|
|
269
|
+
this.reconnectAttempts = 0;
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case "heartbeat":
|
|
274
|
+
this.send(encodePacket({ type: "heartbeat" }));
|
|
275
|
+
break;
|
|
276
|
+
case "event": {
|
|
277
|
+
const args = [...packet.args ?? []];
|
|
278
|
+
if (packet.ack === "data" && packet.id) {
|
|
279
|
+
args.push((...acknowledgement) => {
|
|
280
|
+
this.send(encodePacket({ type: "ack", ackId: packet.id, args: acknowledgement }));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
this.events.emit(packet.name ?? "", ...args);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case "ack": {
|
|
287
|
+
const callback = packet.ackId && this.acknowledgements.get(packet.ackId);
|
|
288
|
+
if (callback && packet.ackId) {
|
|
289
|
+
this.acknowledgements.delete(packet.ackId);
|
|
290
|
+
callback(...packet.args ?? []);
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "disconnect":
|
|
295
|
+
this.closeSocket("booted");
|
|
296
|
+
break;
|
|
297
|
+
case "error":
|
|
298
|
+
this.events.emit("error", packet.reason || "Socket.IO protocol error");
|
|
299
|
+
if (packet.advice === "reconnect") {
|
|
300
|
+
this.closeSocket("server error");
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
case "message":
|
|
304
|
+
case "json":
|
|
305
|
+
this.events.emit("message", packet.data);
|
|
306
|
+
break;
|
|
307
|
+
default:
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
send(packet) {
|
|
312
|
+
if (!this.connected || this.socket?.readyState !== WebSocket.OPEN) {
|
|
313
|
+
this.sendBuffer.push(packet);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.socket.send(packet);
|
|
317
|
+
}
|
|
318
|
+
flush() {
|
|
319
|
+
while (this.sendBuffer.length > 0 && this.socket?.readyState === WebSocket.OPEN) {
|
|
320
|
+
this.socket.send(this.sendBuffer.shift());
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
resetInactivityTimer() {
|
|
324
|
+
if (this.inactivityTimer) {
|
|
325
|
+
clearTimeout(this.inactivityTimer);
|
|
326
|
+
}
|
|
327
|
+
this.inactivityTimer = setTimeout(() => this.closeSocket("timeout"), this.closeTimeoutMs);
|
|
328
|
+
}
|
|
329
|
+
handleClose() {
|
|
330
|
+
const wasConnected = this.connected;
|
|
331
|
+
this.connected = false;
|
|
332
|
+
if (this.inactivityTimer) {
|
|
333
|
+
clearTimeout(this.inactivityTimer);
|
|
334
|
+
}
|
|
335
|
+
if (wasConnected) {
|
|
336
|
+
this.events.emit("disconnect", "transport close");
|
|
337
|
+
}
|
|
338
|
+
this.scheduleReconnect();
|
|
339
|
+
}
|
|
340
|
+
closeSocket(reason) {
|
|
341
|
+
const wasConnected = this.connected;
|
|
342
|
+
this.connected = false;
|
|
343
|
+
if (this.inactivityTimer) {
|
|
344
|
+
clearTimeout(this.inactivityTimer);
|
|
345
|
+
}
|
|
346
|
+
const socket = this.socket;
|
|
347
|
+
this.socket = void 0;
|
|
348
|
+
socket?.close();
|
|
349
|
+
if (wasConnected) {
|
|
350
|
+
this.events.emit("disconnect", reason);
|
|
351
|
+
}
|
|
352
|
+
if (!this.explicitlyDisconnected) {
|
|
353
|
+
this.scheduleReconnect();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
scheduleReconnect() {
|
|
357
|
+
if (!this.reconnectEnabled || this.explicitlyDisconnected || this.reconnectTimer) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const maxAttempts = this.options.maxReconnectionAttempts ?? 10;
|
|
361
|
+
if (this.reconnectAttempts >= maxAttempts) {
|
|
362
|
+
this.events.emit("reconnect_failed");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.reconnectAttempts += 1;
|
|
366
|
+
const initial = this.options.reconnectionDelay ?? 1e3;
|
|
367
|
+
const limit = this.options.reconnectionLimit ?? 16e3;
|
|
368
|
+
const delay = Math.min(initial * 2 ** (this.reconnectAttempts - 1), limit);
|
|
369
|
+
this.events.emit("reconnecting", delay, this.reconnectAttempts);
|
|
370
|
+
this.reconnectTimer = setTimeout(() => {
|
|
371
|
+
this.reconnectTimer = void 0;
|
|
372
|
+
void this.connect();
|
|
373
|
+
}, delay);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
var overleafSocketProtocol = { encodePacket, decodePacket, decodePayload };
|
|
377
|
+
var sink;
|
|
378
|
+
function setLogSink(nextSink) {
|
|
379
|
+
sink = nextSink;
|
|
380
|
+
}
|
|
381
|
+
function write(level, args) {
|
|
382
|
+
if (sink !== void 0) {
|
|
383
|
+
sink(level, args);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const method = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
387
|
+
method(...args);
|
|
388
|
+
}
|
|
389
|
+
function log(...args) {
|
|
390
|
+
write("info", args);
|
|
391
|
+
}
|
|
392
|
+
function warn(...args) {
|
|
393
|
+
write("warn", args);
|
|
394
|
+
}
|
|
395
|
+
function error(...args) {
|
|
396
|
+
write("error", args);
|
|
397
|
+
}
|
|
398
|
+
var globalRequestChain = Promise.resolve();
|
|
399
|
+
var globalNextRequestAt = 0;
|
|
400
|
+
var globalRateLimitedUntil = 0;
|
|
401
|
+
var GLOBAL_MIN_REQUEST_INTERVAL_MS = 300;
|
|
402
|
+
function retryAfterHeaderMs(response) {
|
|
403
|
+
const retryAfter = response.headers?.get?.("retry-after");
|
|
404
|
+
if (retryAfter !== void 0 && retryAfter !== null) {
|
|
405
|
+
const seconds = Number(retryAfter);
|
|
406
|
+
if (Number.isFinite(seconds)) {
|
|
407
|
+
return Math.max(1e3, seconds * 1e3);
|
|
408
|
+
}
|
|
409
|
+
const retryAt = Date.parse(retryAfter);
|
|
410
|
+
if (Number.isFinite(retryAt)) {
|
|
411
|
+
return Math.max(1e3, retryAt - Date.now());
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return 5e3;
|
|
415
|
+
}
|
|
416
|
+
async function queuedFetch(url, init) {
|
|
417
|
+
const previous = globalRequestChain;
|
|
418
|
+
let release;
|
|
419
|
+
globalRequestChain = new Promise((resolve6) => {
|
|
420
|
+
release = resolve6;
|
|
421
|
+
});
|
|
422
|
+
await previous;
|
|
423
|
+
try {
|
|
424
|
+
const delayMs = Math.max(
|
|
425
|
+
0,
|
|
426
|
+
globalNextRequestAt - Date.now(),
|
|
427
|
+
globalRateLimitedUntil - Date.now()
|
|
428
|
+
);
|
|
429
|
+
if (delayMs > 0) {
|
|
430
|
+
await new Promise((resolve6) => setTimeout(resolve6, delayMs));
|
|
431
|
+
}
|
|
432
|
+
globalNextRequestAt = Date.now() + GLOBAL_MIN_REQUEST_INTERVAL_MS;
|
|
433
|
+
const response = await fetch(url, init);
|
|
434
|
+
if (response.status === 429) {
|
|
435
|
+
globalRateLimitedUntil = Math.max(globalRateLimitedUntil, Date.now() + retryAfterHeaderMs(response));
|
|
436
|
+
log(`Global HTTP request queue entered rate-limit cooldown until ${new Date(globalRateLimitedUntil).toISOString()}.`);
|
|
437
|
+
}
|
|
438
|
+
return response;
|
|
439
|
+
} finally {
|
|
440
|
+
release();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function getSetCookie(res) {
|
|
444
|
+
if (typeof res.headers?.getSetCookie === "function") {
|
|
445
|
+
return res.headers.getSetCookie();
|
|
446
|
+
}
|
|
447
|
+
const raw = res.headers?.raw?.()?.["set-cookie"];
|
|
448
|
+
if (raw) {
|
|
449
|
+
return raw;
|
|
450
|
+
}
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
var BaseAPI = class {
|
|
454
|
+
url;
|
|
455
|
+
identity;
|
|
456
|
+
historyRequestChain = Promise.resolve();
|
|
457
|
+
lastHistoryRequestAt = 0;
|
|
458
|
+
isHistoryRoute(route) {
|
|
459
|
+
return route.includes("/updates?") || route.includes("/filetree/diff?") || route.includes("/diff?");
|
|
460
|
+
}
|
|
461
|
+
async waitForHistoryRequest() {
|
|
462
|
+
const previous = this.historyRequestChain;
|
|
463
|
+
let release;
|
|
464
|
+
this.historyRequestChain = new Promise((resolve6) => {
|
|
465
|
+
release = resolve6;
|
|
466
|
+
});
|
|
467
|
+
await previous;
|
|
468
|
+
try {
|
|
469
|
+
const minimumIntervalMs = 3e3;
|
|
470
|
+
const delayMs = Math.max(0, minimumIntervalMs - (Date.now() - this.lastHistoryRequestAt));
|
|
471
|
+
if (delayMs > 0) {
|
|
472
|
+
await new Promise((resolve6) => setTimeout(resolve6, delayMs));
|
|
473
|
+
}
|
|
474
|
+
this.lastHistoryRequestAt = Date.now();
|
|
475
|
+
} finally {
|
|
476
|
+
release();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
retryAfterMs(response, attempt) {
|
|
480
|
+
const retryAfter = response.headers?.get?.("retry-after");
|
|
481
|
+
if (retryAfter !== void 0 && retryAfter !== null) {
|
|
482
|
+
const seconds = Number(retryAfter);
|
|
483
|
+
if (Number.isFinite(seconds)) {
|
|
484
|
+
return Math.max(1e3, seconds * 1e3);
|
|
485
|
+
}
|
|
486
|
+
const retryAt = Date.parse(retryAfter);
|
|
487
|
+
if (Number.isFinite(retryAt)) {
|
|
488
|
+
return Math.max(1e3, retryAt - Date.now());
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return 5e3 * Math.pow(2, attempt);
|
|
492
|
+
}
|
|
493
|
+
constructor(url) {
|
|
494
|
+
this.url = url;
|
|
495
|
+
}
|
|
496
|
+
async getCsrfToken() {
|
|
497
|
+
const res = await queuedFetch(this.url + "login", {
|
|
498
|
+
method: "GET",
|
|
499
|
+
redirect: "manual"
|
|
500
|
+
});
|
|
501
|
+
const body = await res.text();
|
|
502
|
+
const match = body.match(/<input.*name="_csrf".*value="([^"]*)">/);
|
|
503
|
+
if (!match) {
|
|
504
|
+
throw new Error("Failed to get CSRF token.");
|
|
505
|
+
} else {
|
|
506
|
+
const csrfToken = match[1];
|
|
507
|
+
const cookies = getSetCookie(res)[0]?.split(";")[0] ?? "";
|
|
508
|
+
return { csrfToken, cookies };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async getUserId(cookies) {
|
|
512
|
+
const res = await queuedFetch(this.url + "project", {
|
|
513
|
+
method: "GET",
|
|
514
|
+
redirect: "manual",
|
|
515
|
+
headers: {
|
|
516
|
+
"Connection": "keep-alive",
|
|
517
|
+
"Cookie": cookies
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
const body = await res.text();
|
|
521
|
+
const userIDMatch = body.match(/<meta\s+name="ol-user_id"\s+content="([^"]*)">/);
|
|
522
|
+
const userEmailMatch = body.match(/<meta\s+name="ol-usersEmail"\s+content="([^"]*)">/);
|
|
523
|
+
const csrfTokenMatch = body.match(/<meta\s+name="ol-csrfToken"\s+content="([^"]*)">/);
|
|
524
|
+
if (userIDMatch !== null && csrfTokenMatch !== null) {
|
|
525
|
+
const userId = userIDMatch[1];
|
|
526
|
+
const csrfToken = csrfTokenMatch[1];
|
|
527
|
+
const userEmail = userEmailMatch ? userEmailMatch[1] : "";
|
|
528
|
+
return { userId, userEmail, csrfToken };
|
|
529
|
+
} else {
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Reference: "github:overleaf/overleaf/services/web/frontend/js/ide/connection/ConnectionManager.js#L137"
|
|
534
|
+
_initSocketV0(identity, query) {
|
|
535
|
+
const url = new URL(this.url).origin + (query ?? "");
|
|
536
|
+
return new OverleafRealtimeSocket(url, {
|
|
537
|
+
origin: new URL(this.url).origin,
|
|
538
|
+
cookie: identity.cookies,
|
|
539
|
+
reconnect: true,
|
|
540
|
+
reconnectionDelay: 1e3,
|
|
541
|
+
reconnectionLimit: 16e3,
|
|
542
|
+
maxReconnectionAttempts: 10
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
async passportLogin(email, password) {
|
|
546
|
+
const identity = await this.getCsrfToken();
|
|
547
|
+
const res = await queuedFetch(this.url + "login", {
|
|
548
|
+
method: "POST",
|
|
549
|
+
redirect: "manual",
|
|
550
|
+
headers: {
|
|
551
|
+
"Accept": "*/*",
|
|
552
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
553
|
+
"Connection": "keep-alive",
|
|
554
|
+
"Content-Type": "application/json",
|
|
555
|
+
"Cookie": identity.cookies,
|
|
556
|
+
"X-Csrf-Token": identity.csrfToken
|
|
557
|
+
},
|
|
558
|
+
body: JSON.stringify({ _csrf: identity.csrfToken, email, password })
|
|
559
|
+
});
|
|
560
|
+
if (res.status === 302) {
|
|
561
|
+
const redirect = (await res.text()).match(/Found. Redirecting to (.*)/)[1];
|
|
562
|
+
if (redirect === "/project") {
|
|
563
|
+
const cookies = getSetCookie(res)[0] ?? "";
|
|
564
|
+
return await this.cookiesLogin(cookies);
|
|
565
|
+
} else {
|
|
566
|
+
return {
|
|
567
|
+
type: "error",
|
|
568
|
+
message: `Redirecting to /${redirect}`
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
} else if (res.status === 200) {
|
|
572
|
+
return {
|
|
573
|
+
type: "error",
|
|
574
|
+
message: (await res.json()).message.message
|
|
575
|
+
};
|
|
576
|
+
} else if (res.status === 401) {
|
|
577
|
+
return {
|
|
578
|
+
type: "error",
|
|
579
|
+
message: (await res.json()).message.text
|
|
580
|
+
};
|
|
581
|
+
} else {
|
|
582
|
+
return {
|
|
583
|
+
type: "error",
|
|
584
|
+
message: `${res.status}: ` + await res.text()
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async cookiesLogin(cookies) {
|
|
589
|
+
const res = await this.getUserId(cookies);
|
|
590
|
+
if (res) {
|
|
591
|
+
const { userId, userEmail, csrfToken } = res;
|
|
592
|
+
const identity = await this.updateCookies({ cookies, csrfToken });
|
|
593
|
+
return {
|
|
594
|
+
type: "success",
|
|
595
|
+
userInfo: { userId, userEmail },
|
|
596
|
+
identity
|
|
597
|
+
};
|
|
598
|
+
} else {
|
|
599
|
+
return {
|
|
600
|
+
type: "error",
|
|
601
|
+
message: "Failed to get User ID."
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async updateCookies(identity) {
|
|
606
|
+
const res = await queuedFetch(this.url + "socket.io/socket.io.js", {
|
|
607
|
+
method: "GET",
|
|
608
|
+
redirect: "manual",
|
|
609
|
+
headers: {
|
|
610
|
+
"Connection": "keep-alive",
|
|
611
|
+
"Cookie": identity.cookies
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
const cookies = getSetCookie(res)[0]?.split(";")[0];
|
|
615
|
+
if (cookies) {
|
|
616
|
+
identity.cookies = `${identity.cookies}; ${cookies}`;
|
|
617
|
+
}
|
|
618
|
+
return identity;
|
|
619
|
+
}
|
|
620
|
+
setIdentity(identity) {
|
|
621
|
+
this.identity = identity;
|
|
622
|
+
return this;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Check if an HTTP error is transient (worth retrying).
|
|
626
|
+
* Retries on: 5xx server errors, network errors (fetch failures), and 429 rate limiting.
|
|
627
|
+
*/
|
|
628
|
+
isTransientError(statusCode, errorMessage) {
|
|
629
|
+
if (statusCode === void 0) {
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
if (statusCode >= 500 || statusCode === 429) {
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
if (errorMessage && (errorMessage.includes("ECONNRESET") || errorMessage.includes("ETIMEDOUT") || errorMessage.includes("ECONNREFUSED") || errorMessage.includes("ENOTFOUND") || errorMessage.includes("socket hang up"))) {
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
async request(type, route, body, callback, extraHeaders) {
|
|
641
|
+
if (this.identity === void 0) {
|
|
642
|
+
return Promise.reject();
|
|
643
|
+
}
|
|
644
|
+
const MAX_HTTP_RETRIES = 2;
|
|
645
|
+
let lastError = {};
|
|
646
|
+
for (let attempt = 0; attempt <= MAX_HTTP_RETRIES; attempt++) {
|
|
647
|
+
try {
|
|
648
|
+
if (this.isHistoryRoute(route)) {
|
|
649
|
+
await this.waitForHistoryRequest();
|
|
650
|
+
}
|
|
651
|
+
let res = void 0;
|
|
652
|
+
switch (type) {
|
|
653
|
+
case "GET":
|
|
654
|
+
res = await queuedFetch(this.url + route, {
|
|
655
|
+
method: "GET",
|
|
656
|
+
redirect: "manual",
|
|
657
|
+
headers: {
|
|
658
|
+
"Connection": "keep-alive",
|
|
659
|
+
"Cookie": this.identity.cookies,
|
|
660
|
+
...extraHeaders
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
break;
|
|
664
|
+
case "POST":
|
|
665
|
+
const content_type = body instanceof FormData ? void 0 : { "Content-Type": "application/json" };
|
|
666
|
+
const raw_body = body instanceof FormData ? body : JSON.stringify({
|
|
667
|
+
_csrf: this.identity.csrfToken,
|
|
668
|
+
...body
|
|
669
|
+
});
|
|
670
|
+
res = await queuedFetch(this.url + route, {
|
|
671
|
+
method: "POST",
|
|
672
|
+
redirect: "manual",
|
|
673
|
+
headers: {
|
|
674
|
+
"Connection": "keep-alive",
|
|
675
|
+
"Cookie": this.identity.cookies,
|
|
676
|
+
...content_type,
|
|
677
|
+
...extraHeaders
|
|
678
|
+
},
|
|
679
|
+
body: raw_body
|
|
680
|
+
});
|
|
681
|
+
break;
|
|
682
|
+
case "PUT":
|
|
683
|
+
break;
|
|
684
|
+
case "DELETE":
|
|
685
|
+
res = await queuedFetch(this.url + route, {
|
|
686
|
+
method: "DELETE",
|
|
687
|
+
redirect: "manual",
|
|
688
|
+
headers: {
|
|
689
|
+
"Connection": "keep-alive",
|
|
690
|
+
"Cookie": this.identity.cookies,
|
|
691
|
+
"X-Csrf-Token": this.identity.csrfToken,
|
|
692
|
+
...extraHeaders
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
;
|
|
698
|
+
if (res && (res.status === 200 || res.status === 204)) {
|
|
699
|
+
const _res = res.status === 200 ? await res.text() : void 0;
|
|
700
|
+
const response = callback && callback(_res);
|
|
701
|
+
return {
|
|
702
|
+
type: "success",
|
|
703
|
+
...response
|
|
704
|
+
};
|
|
705
|
+
} else if (res && this.isTransientError(res.status) && attempt < MAX_HTTP_RETRIES) {
|
|
706
|
+
const delayMs = res.status === 429 ? this.retryAfterMs(res, attempt) : Math.min(1e3 * Math.pow(2, attempt), 4e3);
|
|
707
|
+
log(`HTTP ${res.status} on ${route}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_HTTP_RETRIES})`);
|
|
708
|
+
lastError = { statusCode: res.status, message: await res.text().catch(() => "") };
|
|
709
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
710
|
+
continue;
|
|
711
|
+
} else {
|
|
712
|
+
const resOrFallback = res || { status: "undefined", text: async () => "" };
|
|
713
|
+
let errorBody = "";
|
|
714
|
+
try {
|
|
715
|
+
errorBody = await resOrFallback.text();
|
|
716
|
+
} catch {
|
|
717
|
+
errorBody = "";
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
type: "error",
|
|
721
|
+
statusCode: typeof resOrFallback.status === "number" ? resOrFallback.status : void 0,
|
|
722
|
+
message: `${resOrFallback.status}: ${errorBody}`
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
} catch (err) {
|
|
726
|
+
const errMsg = err?.message || String(err);
|
|
727
|
+
if (this.isTransientError(void 0, errMsg) && attempt < MAX_HTTP_RETRIES) {
|
|
728
|
+
const delayMs = Math.min(1e3 * Math.pow(2, attempt), 4e3);
|
|
729
|
+
log(`HTTP fetch error on ${route}: ${errMsg}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_HTTP_RETRIES})`);
|
|
730
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
type: "error",
|
|
735
|
+
statusCode: void 0,
|
|
736
|
+
message: errMsg
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
type: "error",
|
|
742
|
+
statusCode: lastError.statusCode,
|
|
743
|
+
message: lastError.message || `Request failed after ${MAX_HTTP_RETRIES + 1} attempts`
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
async download(route) {
|
|
747
|
+
if (this.identity === void 0) {
|
|
748
|
+
return Promise.reject();
|
|
749
|
+
}
|
|
750
|
+
let content = [];
|
|
751
|
+
while (true) {
|
|
752
|
+
const res = await queuedFetch(this.url + route, {
|
|
753
|
+
method: "GET",
|
|
754
|
+
redirect: "manual",
|
|
755
|
+
headers: {
|
|
756
|
+
"Connection": "keep-alive",
|
|
757
|
+
"Cookie": this.identity.cookies
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
if (res.status === 200) {
|
|
761
|
+
content.push(Buffer.from(await res.arrayBuffer()));
|
|
762
|
+
break;
|
|
763
|
+
} else if (res.status === 206) {
|
|
764
|
+
content.push(Buffer.from(await res.arrayBuffer()));
|
|
765
|
+
} else {
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
;
|
|
770
|
+
return Buffer.concat(content);
|
|
771
|
+
}
|
|
772
|
+
async logout(identity) {
|
|
773
|
+
this.setIdentity(identity);
|
|
774
|
+
return this.request("POST", "logout");
|
|
775
|
+
}
|
|
776
|
+
async userProjectsJson(identity) {
|
|
777
|
+
this.setIdentity(identity);
|
|
778
|
+
return this.request("GET", "user/projects", void 0, (res) => {
|
|
779
|
+
const projects = JSON.parse(res).projects;
|
|
780
|
+
projects.forEach((project) => {
|
|
781
|
+
project.id = project._id;
|
|
782
|
+
delete project._id;
|
|
783
|
+
});
|
|
784
|
+
return { projects };
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async getProjectsJson(identity) {
|
|
788
|
+
this.setIdentity(identity);
|
|
789
|
+
return this.request("POST", "api/project", {}, (res) => {
|
|
790
|
+
const projects = JSON.parse(res).projects;
|
|
791
|
+
return { projects };
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
async projectEntitiesJson(identity, projectId) {
|
|
795
|
+
this.setIdentity(identity);
|
|
796
|
+
return this.request("GET", `project/${projectId}/entities`, void 0, (res) => {
|
|
797
|
+
const entities = JSON.parse(res).entities;
|
|
798
|
+
return { entities };
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
async newProject(identity, projectName, template) {
|
|
802
|
+
this.setIdentity(identity);
|
|
803
|
+
return this.request("POST", "project/new", { projectName, template }, (res) => {
|
|
804
|
+
const message = JSON.parse(res).project_id;
|
|
805
|
+
return { message };
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
async cloneProject(identity, projectId, projectName) {
|
|
809
|
+
this.setIdentity(identity);
|
|
810
|
+
return this.request("POST", `project/${projectId}/clone`, { projectName }, (res) => {
|
|
811
|
+
const message = JSON.parse(res).project_id;
|
|
812
|
+
return { message };
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
async renameProject(identity, projectId, newProjectName) {
|
|
816
|
+
this.setIdentity(identity);
|
|
817
|
+
return this.request("POST", `project/${projectId}/rename`, { newProjectName });
|
|
818
|
+
}
|
|
819
|
+
async deleteProject(identity, projectId) {
|
|
820
|
+
this.setIdentity(identity);
|
|
821
|
+
return this.request("DELETE", `project/${projectId}`);
|
|
822
|
+
}
|
|
823
|
+
async archiveProject(identity, projectId) {
|
|
824
|
+
this.setIdentity(identity);
|
|
825
|
+
return this.request(
|
|
826
|
+
"POST",
|
|
827
|
+
`project/${projectId}/archive`,
|
|
828
|
+
void 0,
|
|
829
|
+
void 0,
|
|
830
|
+
{ "X-Csrf-Token": identity.csrfToken }
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
async unarchiveProject(identity, projectId) {
|
|
834
|
+
this.setIdentity(identity);
|
|
835
|
+
return this.request("DELETE", `project/${projectId}/archive`);
|
|
836
|
+
}
|
|
837
|
+
async trashProject(identity, projectId) {
|
|
838
|
+
this.setIdentity(identity);
|
|
839
|
+
return this.request(
|
|
840
|
+
"POST",
|
|
841
|
+
`project/${projectId}/trash`,
|
|
842
|
+
void 0,
|
|
843
|
+
void 0,
|
|
844
|
+
{ "X-Csrf-Token": identity.csrfToken }
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
async untrashProject(identity, projectId) {
|
|
848
|
+
this.setIdentity(identity);
|
|
849
|
+
return this.request("DELETE", `project/${projectId}/trash`);
|
|
850
|
+
}
|
|
851
|
+
async getFile(identity, projectId, fileId) {
|
|
852
|
+
this.setIdentity(identity);
|
|
853
|
+
const content = await this.download(`project/${projectId}/file/${fileId}`);
|
|
854
|
+
return {
|
|
855
|
+
type: "success",
|
|
856
|
+
content: new Uint8Array(content)
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
async addDoc(identity, projectId, parentFolderId, filename) {
|
|
860
|
+
this.setIdentity(identity);
|
|
861
|
+
return this.request("POST", `project/${projectId}/doc`, { parent_folder_id: parentFolderId, name: filename }, (res) => {
|
|
862
|
+
const { _id } = JSON.parse(res);
|
|
863
|
+
const entity = { _type: "doc", _id, name: filename };
|
|
864
|
+
return { entity };
|
|
865
|
+
}, { "X-Csrf-Token": identity.csrfToken });
|
|
866
|
+
}
|
|
867
|
+
async uploadFile(identity, projectId, parentFolderId, filename, fileContent) {
|
|
868
|
+
const formData = new FormData();
|
|
869
|
+
const mimeType = mime.getType(filename);
|
|
870
|
+
formData.append("targetFolderId", parentFolderId);
|
|
871
|
+
formData.append("name", filename);
|
|
872
|
+
formData.append("type", mimeType ? mimeType : "text/plain");
|
|
873
|
+
formData.append("qqfile", new Blob2([Buffer.from(fileContent)], {
|
|
874
|
+
type: mimeType ? mimeType : "application/octet-stream"
|
|
875
|
+
}), filename);
|
|
876
|
+
this.setIdentity(identity);
|
|
877
|
+
return this.request("POST", `project/${projectId}/upload?folder_id=${parentFolderId}`, formData, (res) => {
|
|
878
|
+
const { success, entity_id, entity_type } = JSON.parse(res);
|
|
879
|
+
const entity = { _type: entity_type, _id: entity_id, name: filename };
|
|
880
|
+
return { entity };
|
|
881
|
+
}, { "X-Csrf-Token": identity.csrfToken });
|
|
882
|
+
}
|
|
883
|
+
async uploadProject(identity, filename, fileContent) {
|
|
884
|
+
const uuid = uuidv4();
|
|
885
|
+
const formData = new FormData();
|
|
886
|
+
formData.append("qqfile", new Blob2([Buffer.from(fileContent)], {
|
|
887
|
+
type: "application/zip"
|
|
888
|
+
}), filename);
|
|
889
|
+
this.setIdentity(identity);
|
|
890
|
+
return this.request("POST", `project/new/upload?_csrf=${identity.csrfToken}&qquuid=${uuid}&qqfilename=${filename}&qqtotalfilesize=${fileContent.length}`, formData, (res) => {
|
|
891
|
+
const message = JSON.parse(res).project_id;
|
|
892
|
+
return { message };
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
async addFolder(identity, projectId, folderName, parentFolderId) {
|
|
896
|
+
const body = { name: folderName, parent_folder_id: parentFolderId };
|
|
897
|
+
this.setIdentity(identity);
|
|
898
|
+
return this.request("POST", `project/${projectId}/folder`, body, (res) => {
|
|
899
|
+
const entity = JSON.parse(res);
|
|
900
|
+
return { entity };
|
|
901
|
+
}, { "X-Csrf-Token": identity.csrfToken });
|
|
902
|
+
}
|
|
903
|
+
async deleteEntity(identity, projectId, fileType, fileId) {
|
|
904
|
+
this.setIdentity(identity);
|
|
905
|
+
return this.request("DELETE", `project/${projectId}/${fileType}/${fileId}`);
|
|
906
|
+
}
|
|
907
|
+
async deleteAuxFiles(identity, projectId) {
|
|
908
|
+
this.setIdentity(identity);
|
|
909
|
+
return this.request("DELETE", `project/${projectId}/output`);
|
|
910
|
+
}
|
|
911
|
+
async renameEntity(identity, projectId, entityType, entityId, name) {
|
|
912
|
+
this.setIdentity(identity);
|
|
913
|
+
return this.request(
|
|
914
|
+
"POST",
|
|
915
|
+
`project/${projectId}/${entityType}/${entityId}/rename`,
|
|
916
|
+
{ name },
|
|
917
|
+
void 0,
|
|
918
|
+
{ "X-Csrf-Token": identity.csrfToken }
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
async moveEntity(identity, projectId, entityType, entityId, newParentFolderId) {
|
|
922
|
+
this.setIdentity(identity);
|
|
923
|
+
return this.request(
|
|
924
|
+
"POST",
|
|
925
|
+
`project/${projectId}/${entityType}/${entityId}/move`,
|
|
926
|
+
{ folder_id: newParentFolderId },
|
|
927
|
+
void 0,
|
|
928
|
+
{ "X-Csrf-Token": identity.csrfToken }
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
async compile(identity, projectId, rootResourcePath, draft = false, stopOnFirstError = false) {
|
|
932
|
+
const body = {
|
|
933
|
+
check: "silent",
|
|
934
|
+
draft,
|
|
935
|
+
incrementalCompilesEnabled: true,
|
|
936
|
+
rootResourcePath,
|
|
937
|
+
// file path e.g. "main.tex"
|
|
938
|
+
stopOnFirstError
|
|
939
|
+
};
|
|
940
|
+
this.setIdentity(identity);
|
|
941
|
+
return this.request("POST", `project/${projectId}/compile?auto_compile=true`, body, (res) => {
|
|
942
|
+
const compile = JSON.parse(res);
|
|
943
|
+
return { compile };
|
|
944
|
+
}, { "X-Csrf-Token": identity.csrfToken });
|
|
945
|
+
}
|
|
946
|
+
async stopCompile(identity, projectId) {
|
|
947
|
+
this.setIdentity(identity);
|
|
948
|
+
return this.request("POST", `project/${projectId}/compile/stop`, void 0, void 0, { "X-Csrf-Token": identity.csrfToken });
|
|
949
|
+
}
|
|
950
|
+
async indexAll(identity, projectId) {
|
|
951
|
+
this.setIdentity(identity);
|
|
952
|
+
return this.request("POST", `project/${projectId}/references/indexAll`, { shouldBroadcast: false }, void 0);
|
|
953
|
+
}
|
|
954
|
+
async getMetadata(identity, projectId) {
|
|
955
|
+
this.setIdentity(identity);
|
|
956
|
+
return this.request("GET", `project/${projectId}/metadata`, void 0, (res) => {
|
|
957
|
+
const meta = JSON.parse(res);
|
|
958
|
+
return { meta };
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
async proxyRequestToSpellingApi(identity, language, userId, words) {
|
|
962
|
+
const body = {
|
|
963
|
+
language,
|
|
964
|
+
skipLearnedWords: true,
|
|
965
|
+
token: userId,
|
|
966
|
+
words
|
|
967
|
+
};
|
|
968
|
+
this.setIdentity(identity);
|
|
969
|
+
return this.request("POST", "spelling/check", body, (res) => {
|
|
970
|
+
const misspellings = JSON.parse(res).misspellings;
|
|
971
|
+
return { misspellings };
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
async spellingControllerLearn(identity, userId, word) {
|
|
975
|
+
const body = {
|
|
976
|
+
token: userId,
|
|
977
|
+
word
|
|
978
|
+
};
|
|
979
|
+
this.setIdentity(identity);
|
|
980
|
+
return this.request("POST", "spelling/learn", body);
|
|
981
|
+
}
|
|
982
|
+
async spellingControllerUnlearn(identity, word) {
|
|
983
|
+
this.setIdentity(identity);
|
|
984
|
+
return this.request("POST", "spelling/unlearn", { word }, void 0, { "X-Csrf-Token": identity.csrfToken });
|
|
985
|
+
}
|
|
986
|
+
async getProjectSettings(identity, projectId) {
|
|
987
|
+
this.setIdentity(identity);
|
|
988
|
+
return this.request("GET", `project/${projectId}`, void 0, (res) => {
|
|
989
|
+
const body = res || "";
|
|
990
|
+
const learnedWordsMatch = /<meta\s+name="ol-learnedWords"\s+data-type="json"\s+content="(\[.*?\])">/.exec(body);
|
|
991
|
+
const learnedWords = learnedWordsMatch !== null ? JSON.parse(learnedWordsMatch[1].replace(/"/g, '"')) : [];
|
|
992
|
+
const languagesMatch = /<meta\s+name="ol-languages"\s+data-type="json"\s+content="(\[.*?\])">/.exec(body);
|
|
993
|
+
const languages = languagesMatch !== null ? JSON.parse(languagesMatch[1].replace(/"/g, '"')) : [];
|
|
994
|
+
languages.length && languages.unshift({ name: "Off", code: "" });
|
|
995
|
+
const compilers = [
|
|
996
|
+
{ code: "pdflatex", name: "pdfLaTex" },
|
|
997
|
+
{ code: "latex", name: "LaTex" },
|
|
998
|
+
{ code: "xelatex", name: "XeLaTex" },
|
|
999
|
+
{ code: "lualatex", name: "LuaLaTex" }
|
|
1000
|
+
];
|
|
1001
|
+
const settings = { learnedWords, languages, compilers };
|
|
1002
|
+
return { settings };
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
async updateProjectSettings(identity, projectId, setting) {
|
|
1006
|
+
this.setIdentity(identity);
|
|
1007
|
+
return this.request("POST", `project/${projectId}/settings`, setting);
|
|
1008
|
+
}
|
|
1009
|
+
async getFileFromClsi(identity, url, compileGroup, clsiServerId, pdfDownloadDomain) {
|
|
1010
|
+
if (pdfDownloadDomain && clsiServerId) {
|
|
1011
|
+
const cdnUrl = `${pdfDownloadDomain.replace(/\/+$/, "")}/${url.replace(/^\/+/g, "")}?compileGroup=${encodeURIComponent(compileGroup)}&clsiserverid=${encodeURIComponent(clsiServerId)}&enable_pdf_caching=true`;
|
|
1012
|
+
const content2 = await this._downloadAbsolute(cdnUrl, false);
|
|
1013
|
+
return { type: "success", content: new Uint8Array(content2) };
|
|
1014
|
+
}
|
|
1015
|
+
url = url.replace(/^\/+/g, "");
|
|
1016
|
+
this.setIdentity(identity);
|
|
1017
|
+
const content = await this.download(url);
|
|
1018
|
+
return {
|
|
1019
|
+
type: "success",
|
|
1020
|
+
content: new Uint8Array(content)
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
/** Download from an absolute URL, optionally including web frontend cookies. */
|
|
1024
|
+
async _downloadAbsolute(absoluteUrl, includeCookies) {
|
|
1025
|
+
const headers = {
|
|
1026
|
+
"Connection": "keep-alive"
|
|
1027
|
+
};
|
|
1028
|
+
if (includeCookies && this.identity) {
|
|
1029
|
+
headers["Cookie"] = this.identity.cookies;
|
|
1030
|
+
}
|
|
1031
|
+
let content = [];
|
|
1032
|
+
while (true) {
|
|
1033
|
+
const res = await queuedFetch(absoluteUrl, {
|
|
1034
|
+
method: "GET",
|
|
1035
|
+
redirect: "manual",
|
|
1036
|
+
headers
|
|
1037
|
+
});
|
|
1038
|
+
if (res.status === 200) {
|
|
1039
|
+
content.push(Buffer.from(await res.arrayBuffer()));
|
|
1040
|
+
break;
|
|
1041
|
+
} else if (res.status === 206) {
|
|
1042
|
+
content.push(Buffer.from(await res.arrayBuffer()));
|
|
1043
|
+
} else {
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return Buffer.concat(content);
|
|
1048
|
+
}
|
|
1049
|
+
async proxySyncPdf(identity, projectId, page, h, v, buildId) {
|
|
1050
|
+
this.setIdentity(identity);
|
|
1051
|
+
const request = `project/${projectId}/sync/pdf?page=${page}&h=${h.toFixed(2)}&v=${v.toFixed(2)}&editorId=${uuidv4()}&buildId=${buildId}`;
|
|
1052
|
+
return this.request(
|
|
1053
|
+
"GET",
|
|
1054
|
+
`project/${projectId}/sync/pdf?page=${page}&h=${h.toFixed(2)}&v=${v.toFixed(2)}&editorId=${uuidv4()}&buildId=${buildId}`,
|
|
1055
|
+
void 0,
|
|
1056
|
+
(res) => {
|
|
1057
|
+
const syncPdf = JSON.parse(res).code[0];
|
|
1058
|
+
return { syncPdf };
|
|
1059
|
+
}
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
async proxySyncCode(identity, projectId, file, line, column, buildId) {
|
|
1063
|
+
this.setIdentity(identity);
|
|
1064
|
+
return this.request(
|
|
1065
|
+
"GET",
|
|
1066
|
+
`project/${projectId}/sync/code?file=${file}&line=${line}&column=${column}&editorId=${uuidv4()}&buildId=${buildId}`,
|
|
1067
|
+
void 0,
|
|
1068
|
+
(res) => {
|
|
1069
|
+
const syncCode = JSON.parse(res).pdf;
|
|
1070
|
+
return { syncCode };
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
async getAllTags(identity) {
|
|
1075
|
+
this.setIdentity(identity);
|
|
1076
|
+
return this.request("GET", "tag", void 0, (res) => {
|
|
1077
|
+
const tags = JSON.parse(res);
|
|
1078
|
+
return { tags };
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
async createTag(identity, name) {
|
|
1082
|
+
this.setIdentity(identity);
|
|
1083
|
+
return this.request("POST", "tag", { name }, (res) => {
|
|
1084
|
+
const tags = JSON.parse(res);
|
|
1085
|
+
return { tags };
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
async renameTag(identity, tagId, name) {
|
|
1089
|
+
this.setIdentity(identity);
|
|
1090
|
+
return this.request("POST", `tag/${tagId}/rename`, { name });
|
|
1091
|
+
}
|
|
1092
|
+
async deleteTag(identity, tagId) {
|
|
1093
|
+
this.setIdentity(identity);
|
|
1094
|
+
return this.request("DELETE", `tag/${tagId}`);
|
|
1095
|
+
}
|
|
1096
|
+
async addProjectToTag(identity, tagId, projectId) {
|
|
1097
|
+
this.setIdentity(identity);
|
|
1098
|
+
return this.request("POST", `tag/${tagId}/project/${projectId}`);
|
|
1099
|
+
}
|
|
1100
|
+
async removeProjectFromTag(identity, tagId, projectId) {
|
|
1101
|
+
this.setIdentity(identity);
|
|
1102
|
+
return this.request("DELETE", `tag/${tagId}/project/${projectId}`);
|
|
1103
|
+
}
|
|
1104
|
+
async proxyToHistoryApiAndGetUpdates(identity, projectId, before) {
|
|
1105
|
+
const beforeQuery = before ? `&before=${before}` : "";
|
|
1106
|
+
this.setIdentity(identity);
|
|
1107
|
+
return this.request("GET", `project/${projectId}/updates?min_count=10${beforeQuery}`, void 0, (res) => {
|
|
1108
|
+
const updates = JSON.parse(res);
|
|
1109
|
+
return { updates };
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
async proxyToHistoryApiAndGetFileDiff(identity, projectId, pathname, from, to) {
|
|
1113
|
+
this.setIdentity(identity);
|
|
1114
|
+
return this.request("GET", `project/${projectId}/diff?pathname=${pathname}&from=${from}&to=${to}`, void 0, (res) => {
|
|
1115
|
+
const diff = JSON.parse(res);
|
|
1116
|
+
return { diff };
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
async proxyToHistoryApiAndGetFileTreeDiff(identity, projectId, from, to) {
|
|
1120
|
+
this.setIdentity(identity);
|
|
1121
|
+
return this.request("GET", `project/${projectId}/filetree/diff?from=${from}&to=${to}`, void 0, (res) => {
|
|
1122
|
+
const treeDiff = JSON.parse(res);
|
|
1123
|
+
return { treeDiff };
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
async downloadZipOfVersion(identity, projectId, version) {
|
|
1127
|
+
this.setIdentity(identity);
|
|
1128
|
+
const content = await this.download(`project/${projectId}/version/${version}/zip`);
|
|
1129
|
+
return {
|
|
1130
|
+
type: "success",
|
|
1131
|
+
content: new Uint8Array(content)
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
async getLabels(identity, projectId) {
|
|
1135
|
+
this.setIdentity(identity);
|
|
1136
|
+
return this.request("GET", `project/${projectId}/labels`, void 0, (res) => {
|
|
1137
|
+
const labels = JSON.parse(res);
|
|
1138
|
+
return { labels };
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
async createLabel(identity, projectId, comment, version) {
|
|
1142
|
+
this.setIdentity(identity);
|
|
1143
|
+
return this.request("POST", `project/${projectId}/labels`, { comment, version }, (res) => {
|
|
1144
|
+
const labels = [JSON.parse(res)];
|
|
1145
|
+
return { labels };
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
async deleteLabel(identity, projectId, labelId) {
|
|
1149
|
+
this.setIdentity(identity);
|
|
1150
|
+
return this.request("DELETE", `project/${projectId}/labels/${labelId}`);
|
|
1151
|
+
}
|
|
1152
|
+
async getMessages(identity, projectId, limit = 50) {
|
|
1153
|
+
this.setIdentity(identity);
|
|
1154
|
+
return this.request("GET", `project/${projectId}/messages?limit=${limit}`, void 0, (res) => {
|
|
1155
|
+
const messages = JSON.parse(res);
|
|
1156
|
+
return { messages };
|
|
1157
|
+
}, { "X-Csrf-Token": identity.csrfToken });
|
|
1158
|
+
}
|
|
1159
|
+
async sendMessage(identity, projectId, client_id, content) {
|
|
1160
|
+
this.setIdentity(identity);
|
|
1161
|
+
return this.request("POST", `project/${projectId}/messages`, { client_id, content }, void 0, { "X-Csrf-Token": identity.csrfToken });
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
var ExtendedBaseAPI = class extends BaseAPI {
|
|
1165
|
+
async refreshLinkedFile(identity, project_id, file_id) {
|
|
1166
|
+
this.setIdentity(identity);
|
|
1167
|
+
return await this.request("POST", `project/${project_id}/linked_file/${file_id}/refresh`, { shouldReindexReferences: false }, (res) => {
|
|
1168
|
+
const message = JSON.parse(res).new_file_id;
|
|
1169
|
+
return { message };
|
|
1170
|
+
}, { "X-Csrf-Token": identity.csrfToken });
|
|
1171
|
+
}
|
|
1172
|
+
async createLinkedFile(identity, project_id, parent_folder_id, name, provider, data) {
|
|
1173
|
+
this.setIdentity(identity);
|
|
1174
|
+
return await this.request("POST", `project/${project_id}/linked_file`, { name, parent_folder_id, provider, data }, (res) => {
|
|
1175
|
+
const message = JSON.parse(res).new_file_id;
|
|
1176
|
+
return { message };
|
|
1177
|
+
}, { "X-Csrf-Token": identity.csrfToken });
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
var SOCKET_OPERATION_TIMEOUT_MS = 15e3;
|
|
1181
|
+
function decodePackedUtf8(text) {
|
|
1182
|
+
return Buffer.from(text, "latin1").toString("utf-8");
|
|
1183
|
+
}
|
|
1184
|
+
var SocketIOAPI = class _SocketIOAPI {
|
|
1185
|
+
constructor(url, api, identity, projectId) {
|
|
1186
|
+
this.url = url;
|
|
1187
|
+
this.api = api;
|
|
1188
|
+
this.identity = identity;
|
|
1189
|
+
this.projectId = projectId;
|
|
1190
|
+
this.scheme = "realtime";
|
|
1191
|
+
this.init();
|
|
1192
|
+
}
|
|
1193
|
+
url;
|
|
1194
|
+
api;
|
|
1195
|
+
identity;
|
|
1196
|
+
projectId;
|
|
1197
|
+
static alternativeSocketFactory;
|
|
1198
|
+
scheme;
|
|
1199
|
+
record;
|
|
1200
|
+
_handlers = [];
|
|
1201
|
+
connectedPublicId;
|
|
1202
|
+
socket;
|
|
1203
|
+
emit;
|
|
1204
|
+
/** Track the scheme used when the socket was last initialized */
|
|
1205
|
+
_socketInitScheme;
|
|
1206
|
+
static setAlternativeSocketFactory(factory) {
|
|
1207
|
+
_SocketIOAPI.alternativeSocketFactory = factory;
|
|
1208
|
+
}
|
|
1209
|
+
init() {
|
|
1210
|
+
if (this.socket) {
|
|
1211
|
+
try {
|
|
1212
|
+
if (typeof this.socket.removeAllListeners === "function") {
|
|
1213
|
+
this.socket.removeAllListeners();
|
|
1214
|
+
}
|
|
1215
|
+
if (typeof this.socket.disconnect === "function") {
|
|
1216
|
+
this.socket.disconnect();
|
|
1217
|
+
}
|
|
1218
|
+
} catch {
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
switch (this.scheme) {
|
|
1222
|
+
case "Alt":
|
|
1223
|
+
if (!_SocketIOAPI.alternativeSocketFactory) {
|
|
1224
|
+
throw new Error("The alternative Overleaf connection is not available in this runtime.");
|
|
1225
|
+
}
|
|
1226
|
+
this.socket = _SocketIOAPI.alternativeSocketFactory(
|
|
1227
|
+
this.url,
|
|
1228
|
+
this.api,
|
|
1229
|
+
this.identity,
|
|
1230
|
+
this.projectId,
|
|
1231
|
+
this.record
|
|
1232
|
+
);
|
|
1233
|
+
break;
|
|
1234
|
+
case "realtime":
|
|
1235
|
+
this.record = void 0;
|
|
1236
|
+
const query = `?projectId=${this.projectId}&t=${Date.now()}`;
|
|
1237
|
+
this.socket = this.api._initSocketV0(this.identity, query);
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
this.socket.emit[promisify.custom] = (event, ...args) => {
|
|
1241
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1242
|
+
setTimeout(() => {
|
|
1243
|
+
reject("timeout");
|
|
1244
|
+
}, SOCKET_OPERATION_TIMEOUT_MS);
|
|
1245
|
+
});
|
|
1246
|
+
const waitPromise = new Promise((resolve6, reject) => {
|
|
1247
|
+
this.socket.emit(event, ...args, (err, ...data) => {
|
|
1248
|
+
if (err) {
|
|
1249
|
+
reject(err);
|
|
1250
|
+
} else {
|
|
1251
|
+
resolve6(data);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
});
|
|
1255
|
+
return Promise.race([waitPromise, timeoutPromise]);
|
|
1256
|
+
};
|
|
1257
|
+
this.emit = promisify(this.socket.emit).bind(this.socket);
|
|
1258
|
+
this.initInternalHandlers();
|
|
1259
|
+
this.resumeEventHandlers(this._handlers);
|
|
1260
|
+
this._socketInitScheme = this.scheme;
|
|
1261
|
+
}
|
|
1262
|
+
/** Returns true if the socket needs re-initialization (scheme changed, or socket was never init'd) */
|
|
1263
|
+
get needsReinit() {
|
|
1264
|
+
return this._socketInitScheme !== this.scheme || !this.socket;
|
|
1265
|
+
}
|
|
1266
|
+
get connectionScheme() {
|
|
1267
|
+
return this.scheme;
|
|
1268
|
+
}
|
|
1269
|
+
initInternalHandlers() {
|
|
1270
|
+
this.socket.on("connect", () => {
|
|
1271
|
+
log("SocketIOAPI: connected", { scheme: this.scheme, projectId: this.projectId });
|
|
1272
|
+
});
|
|
1273
|
+
this.socket.on("connect_failed", (connectionError) => {
|
|
1274
|
+
log("SocketIOAPI: connect_failed", { scheme: this.scheme, projectId: this.projectId, error: connectionError });
|
|
1275
|
+
});
|
|
1276
|
+
this.socket.on("disconnect", (reason) => {
|
|
1277
|
+
log("SocketIOAPI: disconnect event", { scheme: this.scheme, projectId: this.projectId, reason });
|
|
1278
|
+
});
|
|
1279
|
+
this.socket.on("reconnecting", (delay, attempt) => {
|
|
1280
|
+
log("SocketIOAPI: reconnecting", { scheme: this.scheme, projectId: this.projectId, delay, attempt });
|
|
1281
|
+
});
|
|
1282
|
+
this.socket.on("reconnect", (transport, attempts) => {
|
|
1283
|
+
log("SocketIOAPI: reconnected", { scheme: this.scheme, projectId: this.projectId, transport, attempts });
|
|
1284
|
+
});
|
|
1285
|
+
this.socket.on("reconnect_failed", (error2) => {
|
|
1286
|
+
log("SocketIOAPI: reconnect_failed", { scheme: this.scheme, projectId: this.projectId, error: error2 });
|
|
1287
|
+
});
|
|
1288
|
+
this.socket.on("forceDisconnect", (message, delay = 10) => {
|
|
1289
|
+
log("SocketIOAPI: forceDisconnect", { message, delay, projectId: this.projectId });
|
|
1290
|
+
});
|
|
1291
|
+
this.socket.on("connectionRejected", (err) => {
|
|
1292
|
+
log("SocketIOAPI: connectionRejected", { scheme: this.scheme, projectId: this.projectId, error: err });
|
|
1293
|
+
if (this.socket.io && typeof this.socket.io.reconnect === "function") {
|
|
1294
|
+
this.socket.io.reconnect(false);
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
this.socket.on("error", (err) => {
|
|
1298
|
+
const message = err?.message || String(err);
|
|
1299
|
+
error(`Overleaf connection error: ${message}`);
|
|
1300
|
+
});
|
|
1301
|
+
if (this.scheme === "realtime") {
|
|
1302
|
+
this.record = new Promise((resolve6, reject) => {
|
|
1303
|
+
this.socket.on("joinProjectResponse", (res) => {
|
|
1304
|
+
const publicId = res.publicId;
|
|
1305
|
+
const project = res.project;
|
|
1306
|
+
this.connectedPublicId = publicId;
|
|
1307
|
+
for (const handler of this._handlers) {
|
|
1308
|
+
handler.onConnectionAccepted?.(publicId);
|
|
1309
|
+
}
|
|
1310
|
+
resolve6(project);
|
|
1311
|
+
});
|
|
1312
|
+
this.socket.on("connectionRejected", (err) => {
|
|
1313
|
+
reject(err?.message || err);
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
disconnect() {
|
|
1319
|
+
this.socket.disconnect();
|
|
1320
|
+
}
|
|
1321
|
+
get handlers() {
|
|
1322
|
+
return this._handlers;
|
|
1323
|
+
}
|
|
1324
|
+
get isUsingAlternativeConnectionScheme() {
|
|
1325
|
+
return this.scheme === "Alt";
|
|
1326
|
+
}
|
|
1327
|
+
toggleAlternativeConnectionScheme(url, updatedRecord) {
|
|
1328
|
+
this.scheme = this.scheme === "Alt" ? "realtime" : "Alt";
|
|
1329
|
+
if (updatedRecord) {
|
|
1330
|
+
this.url = url;
|
|
1331
|
+
this.record = Promise.resolve(updatedRecord);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
resumeEventHandlers(handlers) {
|
|
1335
|
+
this._handlers = [];
|
|
1336
|
+
handlers.forEach((handler) => {
|
|
1337
|
+
this.updateEventHandlers(handler);
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
updateEventHandlers(handlers) {
|
|
1341
|
+
this._handlers.push(handlers);
|
|
1342
|
+
Object.values(handlers).forEach((handler) => {
|
|
1343
|
+
switch (handler) {
|
|
1344
|
+
case handlers.onFileCreated:
|
|
1345
|
+
this.socket.on("reciveNewDoc", (parentFolderId, doc) => {
|
|
1346
|
+
handler(parentFolderId, "doc", doc);
|
|
1347
|
+
});
|
|
1348
|
+
this.socket.on("reciveNewFile", (parentFolderId, file) => {
|
|
1349
|
+
handler(parentFolderId, "file", file);
|
|
1350
|
+
});
|
|
1351
|
+
this.socket.on("reciveNewFolder", (parentFolderId, folder) => {
|
|
1352
|
+
handler(parentFolderId, "folder", folder);
|
|
1353
|
+
});
|
|
1354
|
+
break;
|
|
1355
|
+
case handlers.onFileRenamed:
|
|
1356
|
+
this.socket.on("reciveEntityRename", (entityId, newName) => {
|
|
1357
|
+
handler(entityId, newName);
|
|
1358
|
+
});
|
|
1359
|
+
break;
|
|
1360
|
+
case handlers.onFileRemoved:
|
|
1361
|
+
this.socket.on("removeEntity", (entityId) => {
|
|
1362
|
+
handler(entityId);
|
|
1363
|
+
});
|
|
1364
|
+
break;
|
|
1365
|
+
case handlers.onFileMoved:
|
|
1366
|
+
this.socket.on("reciveEntityMove", (entityId, folderId) => {
|
|
1367
|
+
handler(entityId, folderId);
|
|
1368
|
+
});
|
|
1369
|
+
break;
|
|
1370
|
+
case handlers.onFileChanged:
|
|
1371
|
+
this.socket.on("otUpdateApplied", (update) => {
|
|
1372
|
+
handler(update);
|
|
1373
|
+
});
|
|
1374
|
+
break;
|
|
1375
|
+
case handlers.onDisconnected:
|
|
1376
|
+
this.socket.on("disconnect", () => {
|
|
1377
|
+
handler();
|
|
1378
|
+
});
|
|
1379
|
+
break;
|
|
1380
|
+
case handlers.onConnectionAccepted:
|
|
1381
|
+
this.socket.on("connectionAccepted", (_, publicId) => {
|
|
1382
|
+
handler(publicId);
|
|
1383
|
+
});
|
|
1384
|
+
if (this.connectedPublicId !== void 0) {
|
|
1385
|
+
queueMicrotask(() => handler(this.connectedPublicId));
|
|
1386
|
+
}
|
|
1387
|
+
break;
|
|
1388
|
+
case handlers.onClientUpdated:
|
|
1389
|
+
this.socket.on("clientTracking.clientUpdated", (user) => {
|
|
1390
|
+
handler(user);
|
|
1391
|
+
});
|
|
1392
|
+
break;
|
|
1393
|
+
case handlers.onClientDisconnected:
|
|
1394
|
+
this.socket.on("clientTracking.clientDisconnected", (id) => {
|
|
1395
|
+
handler(id);
|
|
1396
|
+
});
|
|
1397
|
+
break;
|
|
1398
|
+
case handlers.onReceivedMessage:
|
|
1399
|
+
this.socket.on("new-chat-message", (message) => {
|
|
1400
|
+
handler(message);
|
|
1401
|
+
});
|
|
1402
|
+
break;
|
|
1403
|
+
case handlers.onSpellCheckLanguageUpdated:
|
|
1404
|
+
this.socket.on("spellCheckLanguageUpdated", (language) => {
|
|
1405
|
+
handler(language);
|
|
1406
|
+
});
|
|
1407
|
+
break;
|
|
1408
|
+
case handlers.onCompilerUpdated:
|
|
1409
|
+
this.socket.on("compilerUpdated", (compiler) => {
|
|
1410
|
+
handler(compiler);
|
|
1411
|
+
});
|
|
1412
|
+
break;
|
|
1413
|
+
case handlers.onRootDocUpdated:
|
|
1414
|
+
this.socket.on("rootDocUpdated", (rootDocId) => {
|
|
1415
|
+
handler(rootDocId);
|
|
1416
|
+
});
|
|
1417
|
+
break;
|
|
1418
|
+
default:
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
get unSyncFileChanges() {
|
|
1424
|
+
return this.scheme === "Alt" ? this.socket.unSyncedChanges : 0;
|
|
1425
|
+
}
|
|
1426
|
+
async syncFileChanges() {
|
|
1427
|
+
if (this.scheme === "Alt") {
|
|
1428
|
+
return await this.socket.uploadToVFS();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Reference: services/web/frontend/js/ide/connection/ConnectionManager.js#L427
|
|
1433
|
+
* @param {string} projectId - The project id.
|
|
1434
|
+
* @returns {Promise}
|
|
1435
|
+
*/
|
|
1436
|
+
async joinProject(project_id) {
|
|
1437
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1438
|
+
setTimeout(() => {
|
|
1439
|
+
reject("timeout");
|
|
1440
|
+
}, SOCKET_OPERATION_TIMEOUT_MS);
|
|
1441
|
+
});
|
|
1442
|
+
switch (this.scheme) {
|
|
1443
|
+
case "Alt":
|
|
1444
|
+
return Promise.race([
|
|
1445
|
+
this.emit("joinProject", { project_id }).then((returns) => returns[0]),
|
|
1446
|
+
timeoutPromise
|
|
1447
|
+
]);
|
|
1448
|
+
case "realtime":
|
|
1449
|
+
return Promise.race([this.record, timeoutPromise]);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Reference: services/web/frontend/js/ide/editor/Document.js#L500
|
|
1454
|
+
* @param {string} docId - The document id.
|
|
1455
|
+
* @returns {Promise}
|
|
1456
|
+
*/
|
|
1457
|
+
async joinDoc(docId) {
|
|
1458
|
+
return this.emit("joinDoc", docId, { encodeRanges: true }).then((returns) => {
|
|
1459
|
+
const [docLinesAscii, version, updates, ranges] = returns;
|
|
1460
|
+
const docLines = docLinesAscii.map((line) => decodePackedUtf8(line));
|
|
1461
|
+
return { docLines, version, updates, ranges };
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Reference: services/web/frontend/js/ide/editor/Document.js#L591
|
|
1466
|
+
* @param {string} docId - The document id.
|
|
1467
|
+
* @returns {Promise}
|
|
1468
|
+
*/
|
|
1469
|
+
async leaveDoc(docId) {
|
|
1470
|
+
return this.emit("leaveDoc", docId).then(() => {
|
|
1471
|
+
return;
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Reference: services/web/frontend/js/ide/editor/ShareJsDocs.js#L78
|
|
1476
|
+
* @param {string} docId - The document id.
|
|
1477
|
+
* @param {any} update - The changes.
|
|
1478
|
+
* @returns {Promise}
|
|
1479
|
+
*/
|
|
1480
|
+
async applyOtUpdate(docId, update) {
|
|
1481
|
+
try {
|
|
1482
|
+
await this.emit("applyOtUpdate", docId, update);
|
|
1483
|
+
} catch (error2) {
|
|
1484
|
+
let detail;
|
|
1485
|
+
if (error2 instanceof Error) {
|
|
1486
|
+
detail = error2.message;
|
|
1487
|
+
} else if (typeof error2 === "string") {
|
|
1488
|
+
detail = error2;
|
|
1489
|
+
} else {
|
|
1490
|
+
try {
|
|
1491
|
+
detail = JSON.stringify(error2);
|
|
1492
|
+
} catch {
|
|
1493
|
+
detail = String(error2);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
throw new Error(`Overleaf document update rejected: ${detail}`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Reference: services/web/frontend/js/ide/online-users/OnlineUserManager.js#L42
|
|
1501
|
+
* @returns {Promise}
|
|
1502
|
+
*/
|
|
1503
|
+
async getConnectedUsers() {
|
|
1504
|
+
return this.emit("clientTracking.getConnectedUsers").then((returns) => {
|
|
1505
|
+
const [connectedUsers] = returns;
|
|
1506
|
+
return connectedUsers;
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Reference: services/web/frontend/js/ide/online-users/OnlineUserManager.js#L150
|
|
1511
|
+
* @param {string} docId - The document id.
|
|
1512
|
+
* @returns {Promise}
|
|
1513
|
+
*/
|
|
1514
|
+
async updatePosition(doc_id, row, column) {
|
|
1515
|
+
return this.emit("clientTracking.updatePosition", { row, column, doc_id }).then(() => {
|
|
1516
|
+
return;
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
var FolderKeys = {
|
|
1521
|
+
folder: "folders",
|
|
1522
|
+
doc: "docs",
|
|
1523
|
+
file: "fileRefs",
|
|
1524
|
+
outputs: "outputs"
|
|
1525
|
+
};
|
|
1526
|
+
function configPath(environment = process.env) {
|
|
1527
|
+
return environment.LEAFRELAY_CONFIG ? resolve(environment.LEAFRELAY_CONFIG) : join(homedir(), ".leafrelay", "config.json");
|
|
1528
|
+
}
|
|
1529
|
+
function normalizeServerUrl(value) {
|
|
1530
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
1531
|
+
url.pathname = "/";
|
|
1532
|
+
url.search = "";
|
|
1533
|
+
url.hash = "";
|
|
1534
|
+
return url.href;
|
|
1535
|
+
}
|
|
1536
|
+
function serverKey(value) {
|
|
1537
|
+
return new URL(normalizeServerUrl(value)).host;
|
|
1538
|
+
}
|
|
1539
|
+
async function readConfig(path = configPath()) {
|
|
1540
|
+
try {
|
|
1541
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1542
|
+
if (parsed.version !== 1 || typeof parsed.servers !== "object" || parsed.servers === null) {
|
|
1543
|
+
throw new Error(`Unsupported LeafRelay configuration in ${path}`);
|
|
1544
|
+
}
|
|
1545
|
+
return parsed;
|
|
1546
|
+
} catch (error2) {
|
|
1547
|
+
if (error2.code === "ENOENT") {
|
|
1548
|
+
return { version: 1, servers: {} };
|
|
1549
|
+
}
|
|
1550
|
+
throw error2;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async function writeConfig(config, path = configPath()) {
|
|
1554
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
1555
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}
|
|
1556
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1557
|
+
await chmod(path, 384);
|
|
1558
|
+
}
|
|
1559
|
+
async function saveServerSession(session, path = configPath()) {
|
|
1560
|
+
const config = await readConfig(path);
|
|
1561
|
+
config.servers[serverKey(session.url)] = session;
|
|
1562
|
+
await writeConfig(config, path);
|
|
1563
|
+
}
|
|
1564
|
+
async function getServerSession(server, environment = process.env, path = configPath(environment)) {
|
|
1565
|
+
const key = serverKey(server);
|
|
1566
|
+
const config = await readConfig(path);
|
|
1567
|
+
const stored = config.servers[key];
|
|
1568
|
+
const cookie = environment.LEAFRELAY_COOKIE;
|
|
1569
|
+
if (!cookie) {
|
|
1570
|
+
return stored;
|
|
1571
|
+
}
|
|
1572
|
+
return {
|
|
1573
|
+
url: stored?.url ?? normalizeServerUrl(server),
|
|
1574
|
+
userId: stored?.userId ?? "",
|
|
1575
|
+
userEmail: stored?.userEmail ?? "",
|
|
1576
|
+
identity: { cookies: cookie, csrfToken: stored?.identity.csrfToken ?? "" },
|
|
1577
|
+
updatedAt: stored?.updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function projectPath(root, path) {
|
|
1581
|
+
const target = resolve2(root, path.replace(/^\/+/, ""));
|
|
1582
|
+
const relativePath = relative(root, target);
|
|
1583
|
+
if (relativePath.startsWith("..") || relativePath === "..") {
|
|
1584
|
+
throw new Error(`Path escapes the local project: ${path}`);
|
|
1585
|
+
}
|
|
1586
|
+
return target;
|
|
1587
|
+
}
|
|
1588
|
+
function syncPath(root, path) {
|
|
1589
|
+
return `/${relative(root, path).split(sep).join("/")}`;
|
|
1590
|
+
}
|
|
1591
|
+
var NodeReplicaFileSystem = class {
|
|
1592
|
+
constructor(root, ignore) {
|
|
1593
|
+
this.root = root;
|
|
1594
|
+
this.ignore = ignore;
|
|
1595
|
+
}
|
|
1596
|
+
root;
|
|
1597
|
+
ignore;
|
|
1598
|
+
watcher;
|
|
1599
|
+
async listFiles() {
|
|
1600
|
+
const files = [];
|
|
1601
|
+
const visit = async (directory) => {
|
|
1602
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
1603
|
+
const absolute = resolve2(directory, entry.name);
|
|
1604
|
+
const path = syncPath(this.root, absolute);
|
|
1605
|
+
if (this.ignore(path) || entry.name.startsWith(".") || entry.isSymbolicLink()) {
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
if (entry.isDirectory()) {
|
|
1609
|
+
await visit(absolute);
|
|
1610
|
+
} else if (entry.isFile()) {
|
|
1611
|
+
files.push(path);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
await visit(this.root);
|
|
1616
|
+
return files.sort();
|
|
1617
|
+
}
|
|
1618
|
+
async read(path) {
|
|
1619
|
+
const target = projectPath(this.root, path);
|
|
1620
|
+
try {
|
|
1621
|
+
const stat = await lstat(target);
|
|
1622
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
1623
|
+
return void 0;
|
|
1624
|
+
}
|
|
1625
|
+
return new Uint8Array(await readFile2(target));
|
|
1626
|
+
} catch (error2) {
|
|
1627
|
+
if (error2.code === "ENOENT") {
|
|
1628
|
+
return void 0;
|
|
1629
|
+
}
|
|
1630
|
+
throw error2;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
async write(path, content) {
|
|
1634
|
+
const target = projectPath(this.root, path);
|
|
1635
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
1636
|
+
await writeFile2(target, content);
|
|
1637
|
+
}
|
|
1638
|
+
async remove(path) {
|
|
1639
|
+
await rm(projectPath(this.root, path), { force: true, recursive: true });
|
|
1640
|
+
}
|
|
1641
|
+
async watch(onChange) {
|
|
1642
|
+
this.watcher = watch(this.root, {
|
|
1643
|
+
ignoreInitial: true,
|
|
1644
|
+
followSymlinks: false,
|
|
1645
|
+
ignored: (path) => {
|
|
1646
|
+
const relativePath = relative(this.root, path);
|
|
1647
|
+
if (!relativePath) {
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
const normalized = `/${relativePath.split(sep).join("/")}`;
|
|
1651
|
+
return this.ignore(normalized) || normalized.split("/").some((part) => part.startsWith("."));
|
|
1652
|
+
},
|
|
1653
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
1654
|
+
});
|
|
1655
|
+
const handler = (path) => onChange(syncPath(this.root, path));
|
|
1656
|
+
this.watcher.on("add", handler).on("change", handler).on("unlink", handler);
|
|
1657
|
+
await new Promise((resolveReady, reject) => {
|
|
1658
|
+
this.watcher.once("ready", resolveReady).once("error", reject);
|
|
1659
|
+
});
|
|
1660
|
+
return async () => {
|
|
1661
|
+
await this.watcher?.close();
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
function parseProjectUri(value) {
|
|
1666
|
+
const uri = new URL(value);
|
|
1667
|
+
const decodedQuery = decodeURIComponent(uri.search.slice(1));
|
|
1668
|
+
const query = new URLSearchParams(decodedQuery);
|
|
1669
|
+
const projectId = query.get("project");
|
|
1670
|
+
if (!projectId) {
|
|
1671
|
+
throw new Error("The project URI in .overleaf/settings.json does not contain a project ID.");
|
|
1672
|
+
}
|
|
1673
|
+
return {
|
|
1674
|
+
serverName: uri.host,
|
|
1675
|
+
projectId,
|
|
1676
|
+
projectName: decodeURIComponent(uri.pathname.split("/").filter(Boolean)[0] ?? ""),
|
|
1677
|
+
userId: query.get("user") ?? void 0
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
async function readProjectSettings(directory) {
|
|
1681
|
+
const settingsPath = resolve3(directory, ".overleaf", "settings.json");
|
|
1682
|
+
const raw = JSON.parse(await readFile3(settingsPath, "utf8"));
|
|
1683
|
+
if (typeof raw.uri !== "string") {
|
|
1684
|
+
throw new Error(`${settingsPath} does not contain a valid project URI.`);
|
|
1685
|
+
}
|
|
1686
|
+
const parsed = parseProjectUri(raw.uri);
|
|
1687
|
+
const serverName = typeof raw.serverName === "string" ? raw.serverName : parsed.serverName;
|
|
1688
|
+
const localReplica = raw.localReplica;
|
|
1689
|
+
return {
|
|
1690
|
+
uri: raw.uri,
|
|
1691
|
+
serverName,
|
|
1692
|
+
serverUrl: typeof raw.serverUrl === "string" ? raw.serverUrl : `https://${serverName}/`,
|
|
1693
|
+
projectId: parsed.projectId,
|
|
1694
|
+
projectName: typeof raw.projectName === "string" ? raw.projectName : parsed.projectName,
|
|
1695
|
+
userId: parsed.userId,
|
|
1696
|
+
localReplica: { settings: localReplica?.settings ?? {} }
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
var DEFAULT_IGNORE_PATTERNS = [
|
|
1700
|
+
"**/.*",
|
|
1701
|
+
"**/.*/**",
|
|
1702
|
+
"**/*.aux",
|
|
1703
|
+
"**/__latexindent*",
|
|
1704
|
+
"**/*.bbl",
|
|
1705
|
+
"**/*.bcf",
|
|
1706
|
+
"**/*.blg",
|
|
1707
|
+
"**/*.fdb_latexmk",
|
|
1708
|
+
"**/*.fls",
|
|
1709
|
+
"**/*.lof",
|
|
1710
|
+
"**/*.log",
|
|
1711
|
+
"**/*.lot",
|
|
1712
|
+
"**/*.out",
|
|
1713
|
+
"**/*.run.xml",
|
|
1714
|
+
"**/*.synctex(busy)",
|
|
1715
|
+
"**/*.synctex.gz",
|
|
1716
|
+
"**/*.toc",
|
|
1717
|
+
"**/*.xdv",
|
|
1718
|
+
"**/main.pdf",
|
|
1719
|
+
"**/output.pdf"
|
|
1720
|
+
];
|
|
1721
|
+
function createIgnoreMatcher(patterns) {
|
|
1722
|
+
const configured = Array.isArray(patterns) ? patterns.filter((pattern) => typeof pattern === "string") : [];
|
|
1723
|
+
const allPatterns = [...DEFAULT_IGNORE_PATTERNS, ...configured];
|
|
1724
|
+
return (path) => {
|
|
1725
|
+
const normalized = path.replace(/^\/+/, "");
|
|
1726
|
+
return normalized.split("/").some((part) => part.startsWith(".")) || allPatterns.some((pattern) => minimatch(normalized, pattern, { dot: true }));
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function normalizePath(path) {
|
|
1730
|
+
const normalized = `/${posix.normalize(path).replace(/^\/+/, "")}`;
|
|
1731
|
+
if (normalized.includes("/../") || normalized.endsWith("/..")) {
|
|
1732
|
+
throw new Error(`Invalid project path: ${path}`);
|
|
1733
|
+
}
|
|
1734
|
+
return normalized;
|
|
1735
|
+
}
|
|
1736
|
+
function textContent(content) {
|
|
1737
|
+
if (content.includes(0)) {
|
|
1738
|
+
return void 0;
|
|
1739
|
+
}
|
|
1740
|
+
try {
|
|
1741
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
1742
|
+
} catch {
|
|
1743
|
+
return void 0;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
var RemoteProject = class {
|
|
1747
|
+
constructor(url, projectId, cookies) {
|
|
1748
|
+
this.url = url;
|
|
1749
|
+
this.projectId = projectId;
|
|
1750
|
+
this.cookies = cookies;
|
|
1751
|
+
this.api = new BaseAPI(url);
|
|
1752
|
+
}
|
|
1753
|
+
url;
|
|
1754
|
+
projectId;
|
|
1755
|
+
cookies;
|
|
1756
|
+
api;
|
|
1757
|
+
identity;
|
|
1758
|
+
socket;
|
|
1759
|
+
project;
|
|
1760
|
+
entries = /* @__PURE__ */ new Map();
|
|
1761
|
+
idToPath = /* @__PURE__ */ new Map();
|
|
1762
|
+
changeHandler;
|
|
1763
|
+
async connect() {
|
|
1764
|
+
const login = await this.api.cookiesLogin(this.cookies);
|
|
1765
|
+
if (login.type !== "success" || !login.identity) {
|
|
1766
|
+
throw new Error(login.message || `Authentication to ${this.url} failed.`);
|
|
1767
|
+
}
|
|
1768
|
+
this.identity = login.identity;
|
|
1769
|
+
this.socket = new SocketIOAPI(this.url, this.api, this.identity, this.projectId);
|
|
1770
|
+
this.project = await this.socket.joinProject(this.projectId);
|
|
1771
|
+
this.reindex();
|
|
1772
|
+
this.registerEvents();
|
|
1773
|
+
}
|
|
1774
|
+
disconnect() {
|
|
1775
|
+
this.socket?.disconnect();
|
|
1776
|
+
}
|
|
1777
|
+
onChange(handler) {
|
|
1778
|
+
this.changeHandler = handler;
|
|
1779
|
+
}
|
|
1780
|
+
listEntries() {
|
|
1781
|
+
return [...this.entries.values()];
|
|
1782
|
+
}
|
|
1783
|
+
entry(path) {
|
|
1784
|
+
return this.entries.get(normalizePath(path));
|
|
1785
|
+
}
|
|
1786
|
+
async read(path) {
|
|
1787
|
+
const entry = this.requireEntry(path);
|
|
1788
|
+
if (entry.type === "doc") {
|
|
1789
|
+
const result = await this.socket.joinDoc(entry.entity._id);
|
|
1790
|
+
return new TextEncoder().encode(result.docLines.join("\n"));
|
|
1791
|
+
}
|
|
1792
|
+
if (entry.type === "file") {
|
|
1793
|
+
const result = await this.api.getFile(this.identity, this.projectId, entry.entity._id);
|
|
1794
|
+
if (result.type === "success" && result.content) {
|
|
1795
|
+
return result.content;
|
|
1796
|
+
}
|
|
1797
|
+
throw new Error(`Could not download ${path}.`);
|
|
1798
|
+
}
|
|
1799
|
+
throw new Error(`${path} is not a file.`);
|
|
1800
|
+
}
|
|
1801
|
+
async mkdir(path) {
|
|
1802
|
+
const normalized = normalizePath(path);
|
|
1803
|
+
if (this.entries.has(normalized)) {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const { parent, name } = await this.ensureParent(normalized);
|
|
1807
|
+
const result = await this.api.addFolder(this.identity, this.projectId, name, parent._id);
|
|
1808
|
+
if (result.type !== "success" || !result.entity) {
|
|
1809
|
+
throw new Error(result.message || `Could not create ${normalized}.`);
|
|
1810
|
+
}
|
|
1811
|
+
const folder = result.entity;
|
|
1812
|
+
folder._type = "folder";
|
|
1813
|
+
folder.docs ??= [];
|
|
1814
|
+
folder.fileRefs ??= [];
|
|
1815
|
+
folder.folders ??= [];
|
|
1816
|
+
parent.folders.push(folder);
|
|
1817
|
+
this.reindex();
|
|
1818
|
+
}
|
|
1819
|
+
async write(path, content) {
|
|
1820
|
+
const normalized = normalizePath(path);
|
|
1821
|
+
const existing = this.entries.get(normalized);
|
|
1822
|
+
if (existing?.type === "folder") {
|
|
1823
|
+
throw new Error(`${normalized} is a directory.`);
|
|
1824
|
+
}
|
|
1825
|
+
if (existing?.type === "doc") {
|
|
1826
|
+
await this.writeDocument(existing.entity, content);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (existing) {
|
|
1830
|
+
await this.remove(normalized);
|
|
1831
|
+
}
|
|
1832
|
+
const { parent, name } = await this.ensureParent(normalized);
|
|
1833
|
+
const text = textContent(content);
|
|
1834
|
+
if (text !== void 0) {
|
|
1835
|
+
const created = await this.api.addDoc(this.identity, this.projectId, parent._id, name);
|
|
1836
|
+
if (created.type !== "success" || !created.entity) {
|
|
1837
|
+
throw new Error(created.message || `Could not create ${normalized}.`);
|
|
1838
|
+
}
|
|
1839
|
+
const entity2 = created.entity;
|
|
1840
|
+
entity2._type = "doc";
|
|
1841
|
+
parent.docs.push(entity2);
|
|
1842
|
+
this.reindex();
|
|
1843
|
+
if (content.length !== 0) {
|
|
1844
|
+
await this.writeDocument(entity2, content);
|
|
1845
|
+
}
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
const uploaded = await this.api.uploadFile(this.identity, this.projectId, parent._id, name, content);
|
|
1849
|
+
if (uploaded.type !== "success" || !uploaded.entity) {
|
|
1850
|
+
throw new Error(uploaded.message || `Could not upload ${normalized}.`);
|
|
1851
|
+
}
|
|
1852
|
+
const entity = uploaded.entity;
|
|
1853
|
+
entity._type = "file";
|
|
1854
|
+
parent.fileRefs.push(entity);
|
|
1855
|
+
this.reindex();
|
|
1856
|
+
}
|
|
1857
|
+
async remove(path) {
|
|
1858
|
+
const normalized = normalizePath(path);
|
|
1859
|
+
const entry = this.requireEntry(normalized);
|
|
1860
|
+
if (!entry.parent) {
|
|
1861
|
+
throw new Error("The project root cannot be removed.");
|
|
1862
|
+
}
|
|
1863
|
+
const result = await this.api.deleteEntity(this.identity, this.projectId, entry.type, entry.entity._id);
|
|
1864
|
+
if (result.type !== "success") {
|
|
1865
|
+
throw new Error(result.message || `Could not remove ${normalized}.`);
|
|
1866
|
+
}
|
|
1867
|
+
this.removeFromParent(entry);
|
|
1868
|
+
this.reindex();
|
|
1869
|
+
}
|
|
1870
|
+
async getCurrentVersion() {
|
|
1871
|
+
const response = await this.api.proxyToHistoryApiAndGetUpdates(this.identity, this.projectId);
|
|
1872
|
+
const version = response.updates?.updates.at(0)?.toV ?? 0;
|
|
1873
|
+
if (response.type !== "success" || !Number.isInteger(version)) {
|
|
1874
|
+
throw new Error(response.message || "Could not determine the current Overleaf project version.");
|
|
1875
|
+
}
|
|
1876
|
+
return version;
|
|
1877
|
+
}
|
|
1878
|
+
async getFileTreeDiff(from, to) {
|
|
1879
|
+
if (from >= to) {
|
|
1880
|
+
return { diff: [] };
|
|
1881
|
+
}
|
|
1882
|
+
const response = await this.api.proxyToHistoryApiAndGetFileTreeDiff(
|
|
1883
|
+
this.identity,
|
|
1884
|
+
this.projectId,
|
|
1885
|
+
from,
|
|
1886
|
+
to
|
|
1887
|
+
);
|
|
1888
|
+
if (response.type === "success") {
|
|
1889
|
+
return response.treeDiff;
|
|
1890
|
+
}
|
|
1891
|
+
if (response.statusCode === 404) {
|
|
1892
|
+
return void 0;
|
|
1893
|
+
}
|
|
1894
|
+
throw new Error(response.message || "Could not load the Overleaf file-tree history.");
|
|
1895
|
+
}
|
|
1896
|
+
async writeDocument(entity, content) {
|
|
1897
|
+
const text = textContent(content);
|
|
1898
|
+
if (text === void 0) {
|
|
1899
|
+
const path = this.idToPath.get(entity._id);
|
|
1900
|
+
await this.remove(path);
|
|
1901
|
+
await this.write(path, content);
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
const remote = await this.socket.joinDoc(entity._id);
|
|
1905
|
+
const previous = remote.docLines.join("\n");
|
|
1906
|
+
if (previous === text) {
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
let position = 0;
|
|
1910
|
+
const operations = [];
|
|
1911
|
+
for (const part of new DiffMatchPatch().diff_main(previous, text)) {
|
|
1912
|
+
const start = position;
|
|
1913
|
+
if (part[0] !== -1) {
|
|
1914
|
+
position += part[1].length;
|
|
1915
|
+
}
|
|
1916
|
+
if (part[0] === 1) {
|
|
1917
|
+
operations.push({ p: start, i: part[1] });
|
|
1918
|
+
}
|
|
1919
|
+
if (part[0] === -1) {
|
|
1920
|
+
operations.push({ p: start, d: part[1] });
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
const update = {
|
|
1924
|
+
doc: entity._id,
|
|
1925
|
+
v: remote.version,
|
|
1926
|
+
hash: createHash("sha1").update(`blob ${text.length}\0${text}`).digest("hex"),
|
|
1927
|
+
op: operations
|
|
1928
|
+
};
|
|
1929
|
+
await this.socket.applyOtUpdate(entity._id, update);
|
|
1930
|
+
}
|
|
1931
|
+
async ensureParent(path) {
|
|
1932
|
+
const name = posix.basename(path);
|
|
1933
|
+
const parentPath = posix.dirname(path);
|
|
1934
|
+
if (parentPath !== "/" && !this.entries.has(parentPath)) {
|
|
1935
|
+
await this.mkdir(parentPath);
|
|
1936
|
+
}
|
|
1937
|
+
const parentEntry = parentPath === "/" ? this.entries.get("/") : this.entries.get(parentPath);
|
|
1938
|
+
if (!parentEntry || parentEntry.type !== "folder") {
|
|
1939
|
+
throw new Error(`Parent directory ${parentPath} does not exist.`);
|
|
1940
|
+
}
|
|
1941
|
+
return { parent: parentEntry.entity, name };
|
|
1942
|
+
}
|
|
1943
|
+
requireEntry(path) {
|
|
1944
|
+
const normalized = normalizePath(path);
|
|
1945
|
+
const entry = this.entries.get(normalized);
|
|
1946
|
+
if (!entry) {
|
|
1947
|
+
throw new Error(`Remote path not found: ${normalized}`);
|
|
1948
|
+
}
|
|
1949
|
+
return entry;
|
|
1950
|
+
}
|
|
1951
|
+
reindex() {
|
|
1952
|
+
this.entries.clear();
|
|
1953
|
+
this.idToPath.clear();
|
|
1954
|
+
const root = this.project.rootFolder[0];
|
|
1955
|
+
root._type = "folder";
|
|
1956
|
+
this.indexFolder(root, "/");
|
|
1957
|
+
}
|
|
1958
|
+
indexFolder(folder, path, parent) {
|
|
1959
|
+
this.entries.set(path, { path, type: "folder", entity: folder, parent });
|
|
1960
|
+
this.idToPath.set(folder._id, path);
|
|
1961
|
+
for (const child of folder.folders ?? []) {
|
|
1962
|
+
child._type = "folder";
|
|
1963
|
+
this.indexFolder(child, normalizePath(posix.join(path, child.name)), folder);
|
|
1964
|
+
}
|
|
1965
|
+
for (const child of folder.docs ?? []) {
|
|
1966
|
+
child._type = "doc";
|
|
1967
|
+
const childPath = normalizePath(posix.join(path, child.name));
|
|
1968
|
+
this.entries.set(childPath, { path: childPath, type: "doc", entity: child, parent: folder });
|
|
1969
|
+
this.idToPath.set(child._id, childPath);
|
|
1970
|
+
}
|
|
1971
|
+
for (const child of folder.fileRefs ?? []) {
|
|
1972
|
+
child._type = "file";
|
|
1973
|
+
const childPath = normalizePath(posix.join(path, child.name));
|
|
1974
|
+
this.entries.set(childPath, { path: childPath, type: "file", entity: child, parent: folder });
|
|
1975
|
+
this.idToPath.set(child._id, childPath);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
folderById(id) {
|
|
1979
|
+
const path = this.idToPath.get(id);
|
|
1980
|
+
const entry = path ? this.entries.get(path) : void 0;
|
|
1981
|
+
return entry?.type === "folder" ? entry.entity : void 0;
|
|
1982
|
+
}
|
|
1983
|
+
removeFromParent(entry) {
|
|
1984
|
+
if (!entry.parent) {
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
const collection = entry.type === "folder" ? entry.parent.folders : entry.type === "doc" ? entry.parent.docs : entry.parent.fileRefs;
|
|
1988
|
+
const index = collection.findIndex((item) => item._id === entry.entity._id);
|
|
1989
|
+
if (index !== -1) {
|
|
1990
|
+
collection.splice(index, 1);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
emitChange(path, previousPath) {
|
|
1994
|
+
this.changeHandler?.({ path: normalizePath(path), previousPath });
|
|
1995
|
+
}
|
|
1996
|
+
registerEvents() {
|
|
1997
|
+
this.socket.updateEventHandlers({
|
|
1998
|
+
onFileCreated: (parentId, type, entity) => {
|
|
1999
|
+
const parent = this.folderById(parentId);
|
|
2000
|
+
if (!parent || type === "outputs") {
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (this.idToPath.has(entity._id)) {
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
entity._type = type;
|
|
2007
|
+
if (type === "folder") {
|
|
2008
|
+
const folder = entity;
|
|
2009
|
+
folder.docs ??= [];
|
|
2010
|
+
folder.fileRefs ??= [];
|
|
2011
|
+
folder.folders ??= [];
|
|
2012
|
+
parent.folders.push(folder);
|
|
2013
|
+
} else if (type === "doc") {
|
|
2014
|
+
parent.docs.push(entity);
|
|
2015
|
+
} else {
|
|
2016
|
+
parent.fileRefs.push(entity);
|
|
2017
|
+
}
|
|
2018
|
+
const parentPath = this.idToPath.get(parentId) ?? "/";
|
|
2019
|
+
this.reindex();
|
|
2020
|
+
this.emitChange(posix.join(parentPath, entity.name));
|
|
2021
|
+
},
|
|
2022
|
+
onFileRenamed: (entityId, newName) => {
|
|
2023
|
+
const previousPath = this.idToPath.get(entityId);
|
|
2024
|
+
if (!previousPath) {
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
this.entries.get(previousPath).entity.name = newName;
|
|
2028
|
+
this.reindex();
|
|
2029
|
+
this.emitChange(this.idToPath.get(entityId), previousPath);
|
|
2030
|
+
},
|
|
2031
|
+
onFileRemoved: (entityId) => {
|
|
2032
|
+
const previousPath = this.idToPath.get(entityId);
|
|
2033
|
+
if (!previousPath) {
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
const entry = this.entries.get(previousPath);
|
|
2037
|
+
this.removeFromParent(entry);
|
|
2038
|
+
this.reindex();
|
|
2039
|
+
this.emitChange(previousPath);
|
|
2040
|
+
},
|
|
2041
|
+
onFileMoved: (entityId, parentId) => {
|
|
2042
|
+
const previousPath = this.idToPath.get(entityId);
|
|
2043
|
+
const nextParent = this.folderById(parentId);
|
|
2044
|
+
if (!previousPath || !nextParent) {
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const entry = this.entries.get(previousPath);
|
|
2048
|
+
this.removeFromParent(entry);
|
|
2049
|
+
if (entry.type === "folder") {
|
|
2050
|
+
nextParent.folders.push(entry.entity);
|
|
2051
|
+
} else if (entry.type === "doc") {
|
|
2052
|
+
nextParent.docs.push(entry.entity);
|
|
2053
|
+
} else {
|
|
2054
|
+
nextParent.fileRefs.push(entry.entity);
|
|
2055
|
+
}
|
|
2056
|
+
this.reindex();
|
|
2057
|
+
this.emitChange(this.idToPath.get(entityId), previousPath);
|
|
2058
|
+
},
|
|
2059
|
+
onFileChanged: (update) => {
|
|
2060
|
+
const path = this.idToPath.get(update.doc);
|
|
2061
|
+
if (path) {
|
|
2062
|
+
this.emitChange(path);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
var textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
2069
|
+
var textEncoder = new TextEncoder();
|
|
2070
|
+
function decodeText(content) {
|
|
2071
|
+
if (content.includes(0)) {
|
|
2072
|
+
return void 0;
|
|
2073
|
+
}
|
|
2074
|
+
try {
|
|
2075
|
+
return textDecoder.decode(content);
|
|
2076
|
+
} catch {
|
|
2077
|
+
return void 0;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function splitLines(content) {
|
|
2081
|
+
return content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
2082
|
+
}
|
|
2083
|
+
function encodeTextBase(content) {
|
|
2084
|
+
return decodeText(content);
|
|
2085
|
+
}
|
|
2086
|
+
function mergeText(baseContent, localContent, remoteContent) {
|
|
2087
|
+
const base = decodeText(baseContent);
|
|
2088
|
+
const local = decodeText(localContent);
|
|
2089
|
+
const remote = decodeText(remoteContent);
|
|
2090
|
+
if (base === void 0 || local === void 0 || remote === void 0) {
|
|
2091
|
+
return { status: "binary" };
|
|
2092
|
+
}
|
|
2093
|
+
const regions = diff3Merge(
|
|
2094
|
+
splitLines(local),
|
|
2095
|
+
splitLines(base),
|
|
2096
|
+
splitLines(remote),
|
|
2097
|
+
{ excludeFalseConflicts: true }
|
|
2098
|
+
);
|
|
2099
|
+
if (regions.some((region) => region.conflict !== void 0)) {
|
|
2100
|
+
return { status: "conflict" };
|
|
2101
|
+
}
|
|
2102
|
+
return {
|
|
2103
|
+
status: "merged",
|
|
2104
|
+
content: textEncoder.encode(regions.flatMap((region) => region.ok ?? []).join(""))
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
var SYNC_STATE_SCHEMA_VERSION = 2;
|
|
2108
|
+
function sha256(content) {
|
|
2109
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
2110
|
+
}
|
|
2111
|
+
function parseSyncState(serialized, projectUri) {
|
|
2112
|
+
try {
|
|
2113
|
+
const state = JSON.parse(serialized);
|
|
2114
|
+
if (state.schemaVersion !== 1 && state.schemaVersion !== SYNC_STATE_SCHEMA_VERSION || state.projectUri !== projectUri || !Number.isInteger(state.remoteVersion) || typeof state.files !== "object" || state.files === null) {
|
|
2115
|
+
return void 0;
|
|
2116
|
+
}
|
|
2117
|
+
return {
|
|
2118
|
+
...state,
|
|
2119
|
+
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
2120
|
+
textBases: typeof state.textBases === "object" && state.textBases !== null ? state.textBases : {}
|
|
2121
|
+
};
|
|
2122
|
+
} catch {
|
|
2123
|
+
return void 0;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
function createSyncState(projectUri, remoteVersion) {
|
|
2127
|
+
return {
|
|
2128
|
+
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
2129
|
+
projectUri,
|
|
2130
|
+
remoteVersion,
|
|
2131
|
+
files: {},
|
|
2132
|
+
textBases: {}
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function updateSyncCheckpoint(state, path, content) {
|
|
2136
|
+
state.files[path] = sha256(content);
|
|
2137
|
+
const textBase = encodeTextBase(content);
|
|
2138
|
+
if (textBase === void 0) {
|
|
2139
|
+
delete state.textBases[path];
|
|
2140
|
+
} else {
|
|
2141
|
+
state.textBases[path] = textBase;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
function removeSyncCheckpoint(state, path) {
|
|
2145
|
+
for (const checkpointPath of Object.keys(state.files)) {
|
|
2146
|
+
if (checkpointPath === path || checkpointPath.startsWith(`${path}/`)) {
|
|
2147
|
+
delete state.files[checkpointPath];
|
|
2148
|
+
delete state.textBases[checkpointPath];
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
function readTextBase(state, path) {
|
|
2153
|
+
const textBase = state?.textBases[path];
|
|
2154
|
+
return textBase === void 0 ? void 0 : new TextEncoder().encode(textBase);
|
|
2155
|
+
}
|
|
2156
|
+
function normalizePath2(path) {
|
|
2157
|
+
return `/${posix2.normalize(path).replace(/^\/+/, "")}`;
|
|
2158
|
+
}
|
|
2159
|
+
function isRemoteFile(entry) {
|
|
2160
|
+
return entry?.type === "doc" || entry?.type === "file";
|
|
2161
|
+
}
|
|
2162
|
+
var SyncEngine = class {
|
|
2163
|
+
constructor(options) {
|
|
2164
|
+
this.options = options;
|
|
2165
|
+
}
|
|
2166
|
+
options;
|
|
2167
|
+
state;
|
|
2168
|
+
queue = Promise.resolve();
|
|
2169
|
+
conflicts = /* @__PURE__ */ new Set();
|
|
2170
|
+
stopLocalWatch;
|
|
2171
|
+
eventTimers = /* @__PURE__ */ new Map();
|
|
2172
|
+
async start() {
|
|
2173
|
+
this.state = await this.options.stateStore.load();
|
|
2174
|
+
const currentVersion = await this.options.remote.getCurrentVersion();
|
|
2175
|
+
const localFiles = await this.options.local.listFiles();
|
|
2176
|
+
const remoteFiles = this.remoteFilePaths();
|
|
2177
|
+
if (!this.state) {
|
|
2178
|
+
this.state = createSyncState(this.options.projectUri, currentVersion);
|
|
2179
|
+
if (localFiles.length !== 0 && remoteFiles.length !== 0) {
|
|
2180
|
+
throw new Error("Synchronization paused: both the local and Overleaf projects contain files, but no common checkpoint exists.");
|
|
2181
|
+
}
|
|
2182
|
+
const source = localFiles.length === 0 ? "remote" : "local";
|
|
2183
|
+
for (const path of source === "remote" ? remoteFiles : localFiles) {
|
|
2184
|
+
await this.reconcilePath(path, source);
|
|
2185
|
+
}
|
|
2186
|
+
this.state.remoteVersion = currentVersion;
|
|
2187
|
+
await this.options.stateStore.save(this.state);
|
|
2188
|
+
} else {
|
|
2189
|
+
await this.startupReconcile(currentVersion, localFiles, remoteFiles);
|
|
2190
|
+
}
|
|
2191
|
+
this.stopLocalWatch = await this.options.local.watch((path) => this.schedule(path, "local"));
|
|
2192
|
+
this.options.remote.onChange((change) => {
|
|
2193
|
+
if (change.previousPath) {
|
|
2194
|
+
this.schedule(change.previousPath, "remote");
|
|
2195
|
+
}
|
|
2196
|
+
this.schedule(change.path, "remote");
|
|
2197
|
+
});
|
|
2198
|
+
this.options.log(`Synchronization active at Overleaf version ${this.state.remoteVersion}.`);
|
|
2199
|
+
}
|
|
2200
|
+
async stop() {
|
|
2201
|
+
for (const timer of this.eventTimers.values()) {
|
|
2202
|
+
clearTimeout(timer);
|
|
2203
|
+
}
|
|
2204
|
+
this.eventTimers.clear();
|
|
2205
|
+
await this.stopLocalWatch?.();
|
|
2206
|
+
await this.queue;
|
|
2207
|
+
}
|
|
2208
|
+
async startupReconcile(currentVersion, localFiles, remoteFiles) {
|
|
2209
|
+
const state = this.state;
|
|
2210
|
+
if (state.remoteVersion > currentVersion) {
|
|
2211
|
+
throw new Error("Synchronization paused: the local checkpoint is newer than the Overleaf project history.");
|
|
2212
|
+
}
|
|
2213
|
+
const localHashes = /* @__PURE__ */ new Map();
|
|
2214
|
+
for (const path of localFiles) {
|
|
2215
|
+
const content = await this.options.local.read(path);
|
|
2216
|
+
if (content) {
|
|
2217
|
+
localHashes.set(path, sha256(content));
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
const localChanged = /* @__PURE__ */ new Set();
|
|
2221
|
+
for (const path of /* @__PURE__ */ new Set([...Object.keys(state.files), ...localFiles])) {
|
|
2222
|
+
if (state.files[path] !== localHashes.get(path)) {
|
|
2223
|
+
localChanged.add(path);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
let remoteDiff = { diff: [] };
|
|
2227
|
+
if (state.remoteVersion < currentVersion) {
|
|
2228
|
+
remoteDiff = await this.options.remote.getFileTreeDiff(state.remoteVersion, currentVersion);
|
|
2229
|
+
if (!remoteDiff) {
|
|
2230
|
+
throw new Error("Synchronization paused: Overleaf history is unavailable, so remote changes cannot be distinguished safely.");
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
const candidates = new Set(localChanged);
|
|
2234
|
+
const knownFiles = /* @__PURE__ */ new Set([...Object.keys(state.files), ...localFiles, ...remoteFiles]);
|
|
2235
|
+
for (const change of remoteDiff.diff) {
|
|
2236
|
+
const paths = [change.pathname, change.newPathname].filter((path) => Boolean(path));
|
|
2237
|
+
for (const changedPath of paths.map(normalizePath2)) {
|
|
2238
|
+
let matched = false;
|
|
2239
|
+
for (const filePath of knownFiles) {
|
|
2240
|
+
if (filePath === changedPath || filePath.startsWith(`${changedPath}/`)) {
|
|
2241
|
+
candidates.add(filePath);
|
|
2242
|
+
matched = true;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (!matched) {
|
|
2246
|
+
candidates.add(changedPath);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
let failed = false;
|
|
2251
|
+
for (const path of [...candidates].sort()) {
|
|
2252
|
+
try {
|
|
2253
|
+
await this.reconcilePath(path, localChanged.has(path) ? "local" : "remote");
|
|
2254
|
+
} catch (error2) {
|
|
2255
|
+
failed = true;
|
|
2256
|
+
this.options.log(`Failed to synchronize ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
if (failed || this.conflicts.size !== 0) {
|
|
2260
|
+
throw new Error(`Synchronization paused for ${failed ? "failed" : "conflicting"} paths; successful paths were checkpointed.`);
|
|
2261
|
+
}
|
|
2262
|
+
state.remoteVersion = currentVersion;
|
|
2263
|
+
await this.options.stateStore.save(state);
|
|
2264
|
+
}
|
|
2265
|
+
schedule(path, source) {
|
|
2266
|
+
const normalized = normalizePath2(path);
|
|
2267
|
+
if (this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
const previous = this.eventTimers.get(normalized);
|
|
2271
|
+
if (previous) {
|
|
2272
|
+
clearTimeout(previous);
|
|
2273
|
+
}
|
|
2274
|
+
this.eventTimers.set(normalized, setTimeout(() => {
|
|
2275
|
+
this.eventTimers.delete(normalized);
|
|
2276
|
+
this.queue = this.queue.then(() => this.reconcilePath(normalized, source)).catch((error2) => {
|
|
2277
|
+
this.options.log(`Failed to synchronize ${normalized}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
2278
|
+
});
|
|
2279
|
+
}, 250));
|
|
2280
|
+
}
|
|
2281
|
+
async reconcilePath(path, source) {
|
|
2282
|
+
const normalized = normalizePath2(path);
|
|
2283
|
+
if (this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
const remoteEntry = this.options.remote.entry(normalized);
|
|
2287
|
+
if (remoteEntry?.type === "folder") {
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
const localContent = await this.options.local.read(normalized);
|
|
2291
|
+
const remoteContent = isRemoteFile(remoteEntry) ? await this.options.remote.read(normalized) : void 0;
|
|
2292
|
+
const baseHash = this.state.files[normalized];
|
|
2293
|
+
const localHash = localContent && sha256(localContent);
|
|
2294
|
+
const remoteHash = remoteContent && sha256(remoteContent);
|
|
2295
|
+
if (localHash === remoteHash) {
|
|
2296
|
+
if (localContent) {
|
|
2297
|
+
updateSyncCheckpoint(this.state, normalized, localContent);
|
|
2298
|
+
} else {
|
|
2299
|
+
removeSyncCheckpoint(this.state, normalized);
|
|
2300
|
+
}
|
|
2301
|
+
await this.options.stateStore.save(this.state);
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
const localChanged = localHash !== baseHash;
|
|
2305
|
+
const remoteChanged = remoteHash !== baseHash;
|
|
2306
|
+
if (localChanged && remoteChanged) {
|
|
2307
|
+
const base = readTextBase(this.state, normalized);
|
|
2308
|
+
if (base && localContent && remoteContent) {
|
|
2309
|
+
const merged = mergeText(base, localContent, remoteContent);
|
|
2310
|
+
if (merged.status === "merged" && merged.content) {
|
|
2311
|
+
await this.options.local.write(normalized, merged.content);
|
|
2312
|
+
await this.options.remote.write(normalized, merged.content);
|
|
2313
|
+
updateSyncCheckpoint(this.state, normalized, merged.content);
|
|
2314
|
+
await this.options.stateStore.save(this.state);
|
|
2315
|
+
this.options.log(`Merged non-overlapping local and Overleaf changes in ${normalized}.`);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
this.conflicts.add(normalized);
|
|
2320
|
+
const reason = "Both local and Overleaf content changed since the last synchronized checkpoint.";
|
|
2321
|
+
this.options.onConflict(normalized, reason);
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
if (localChanged || !remoteChanged && source === "local") {
|
|
2325
|
+
if (!localContent) {
|
|
2326
|
+
if (remoteEntry) {
|
|
2327
|
+
await this.options.remote.remove(normalized);
|
|
2328
|
+
}
|
|
2329
|
+
removeSyncCheckpoint(this.state, normalized);
|
|
2330
|
+
} else {
|
|
2331
|
+
await this.options.remote.write(normalized, localContent);
|
|
2332
|
+
const synchronized = await this.options.remote.read(normalized);
|
|
2333
|
+
if (sha256(synchronized) !== sha256(localContent)) {
|
|
2334
|
+
await this.options.local.write(normalized, synchronized);
|
|
2335
|
+
}
|
|
2336
|
+
updateSyncCheckpoint(this.state, normalized, synchronized);
|
|
2337
|
+
}
|
|
2338
|
+
await this.options.stateStore.save(this.state);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (!remoteContent) {
|
|
2342
|
+
if (localContent) {
|
|
2343
|
+
await this.options.local.remove(normalized);
|
|
2344
|
+
}
|
|
2345
|
+
removeSyncCheckpoint(this.state, normalized);
|
|
2346
|
+
} else {
|
|
2347
|
+
await this.options.local.write(normalized, remoteContent);
|
|
2348
|
+
updateSyncCheckpoint(this.state, normalized, remoteContent);
|
|
2349
|
+
}
|
|
2350
|
+
await this.options.stateStore.save(this.state);
|
|
2351
|
+
}
|
|
2352
|
+
remoteFilePaths() {
|
|
2353
|
+
return this.options.remote.listEntries().filter((entry) => isRemoteFile(entry) && !this.shouldIgnore(entry.path)).map((entry) => entry.path);
|
|
2354
|
+
}
|
|
2355
|
+
shouldIgnore(path) {
|
|
2356
|
+
return this.options.ignore(path) || path.split("/").some((part) => part.startsWith("."));
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
var FileSyncStateStore = class {
|
|
2360
|
+
constructor(directory, projectUri) {
|
|
2361
|
+
this.projectUri = projectUri;
|
|
2362
|
+
this.path = resolve4(directory, ".overleaf", "sync-state.json");
|
|
2363
|
+
}
|
|
2364
|
+
projectUri;
|
|
2365
|
+
path;
|
|
2366
|
+
serialized;
|
|
2367
|
+
async load() {
|
|
2368
|
+
try {
|
|
2369
|
+
const content = await readFile4(this.path, "utf8");
|
|
2370
|
+
const state = parseSyncState(content, this.projectUri);
|
|
2371
|
+
if (state) {
|
|
2372
|
+
this.serialized = `${JSON.stringify(state, null, 2)}
|
|
2373
|
+
`;
|
|
2374
|
+
}
|
|
2375
|
+
return state;
|
|
2376
|
+
} catch (error2) {
|
|
2377
|
+
if (error2.code === "ENOENT") {
|
|
2378
|
+
return void 0;
|
|
2379
|
+
}
|
|
2380
|
+
throw error2;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
async save(state) {
|
|
2384
|
+
const serialized = `${JSON.stringify(state, null, 2)}
|
|
2385
|
+
`;
|
|
2386
|
+
if (serialized === this.serialized) {
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
await mkdir3(dirname3(this.path), { recursive: true });
|
|
2390
|
+
await writeFile3(this.path, serialized, "utf8");
|
|
2391
|
+
this.serialized = serialized;
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
async function startServe(directory = process.cwd()) {
|
|
2395
|
+
const root = resolve5(directory);
|
|
2396
|
+
const settings = await readProjectSettings(root);
|
|
2397
|
+
const session = await getServerSession(settings.serverName);
|
|
2398
|
+
if (!session) {
|
|
2399
|
+
throw new Error(`No login is stored for ${settings.serverName}. Run: leafrelay login ${settings.serverName}`);
|
|
2400
|
+
}
|
|
2401
|
+
const cookie = process.env.LEAFRELAY_COOKIE || session.identity.cookies;
|
|
2402
|
+
const remote = new RemoteProject(session.url || settings.serverUrl, settings.projectId, cookie);
|
|
2403
|
+
await remote.connect();
|
|
2404
|
+
const configuredPatterns = settings.localReplica.settings["ignore-patterns"];
|
|
2405
|
+
const ignore = createIgnoreMatcher(configuredPatterns);
|
|
2406
|
+
const local = new NodeReplicaFileSystem(root, ignore);
|
|
2407
|
+
const engine = new SyncEngine({
|
|
2408
|
+
projectUri: settings.uri,
|
|
2409
|
+
local,
|
|
2410
|
+
remote,
|
|
2411
|
+
stateStore: new FileSyncStateStore(root, settings.uri),
|
|
2412
|
+
ignore,
|
|
2413
|
+
log,
|
|
2414
|
+
onConflict: (path, reason) => {
|
|
2415
|
+
log(`Conflict paused for ${path}: ${reason}`);
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
try {
|
|
2419
|
+
await engine.start();
|
|
2420
|
+
} catch (error2) {
|
|
2421
|
+
remote.disconnect();
|
|
2422
|
+
throw error2;
|
|
2423
|
+
}
|
|
2424
|
+
log(`LeafRelay is serving ${root} (${settings.projectName}).`);
|
|
2425
|
+
return {
|
|
2426
|
+
stop: async () => {
|
|
2427
|
+
await engine.stop();
|
|
2428
|
+
remote.disconnect();
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
export {
|
|
2434
|
+
OverleafRealtimeSocket,
|
|
2435
|
+
overleafSocketProtocol,
|
|
2436
|
+
setLogSink,
|
|
2437
|
+
log,
|
|
2438
|
+
warn,
|
|
2439
|
+
error,
|
|
2440
|
+
BaseAPI,
|
|
2441
|
+
ExtendedBaseAPI,
|
|
2442
|
+
SocketIOAPI,
|
|
2443
|
+
FolderKeys,
|
|
2444
|
+
configPath,
|
|
2445
|
+
normalizeServerUrl,
|
|
2446
|
+
serverKey,
|
|
2447
|
+
readConfig,
|
|
2448
|
+
writeConfig,
|
|
2449
|
+
saveServerSession,
|
|
2450
|
+
getServerSession,
|
|
2451
|
+
NodeReplicaFileSystem,
|
|
2452
|
+
readProjectSettings,
|
|
2453
|
+
DEFAULT_IGNORE_PATTERNS,
|
|
2454
|
+
createIgnoreMatcher,
|
|
2455
|
+
RemoteProject,
|
|
2456
|
+
encodeTextBase,
|
|
2457
|
+
mergeText,
|
|
2458
|
+
SYNC_STATE_SCHEMA_VERSION,
|
|
2459
|
+
sha256,
|
|
2460
|
+
parseSyncState,
|
|
2461
|
+
createSyncState,
|
|
2462
|
+
updateSyncCheckpoint,
|
|
2463
|
+
removeSyncCheckpoint,
|
|
2464
|
+
readTextBase,
|
|
2465
|
+
SyncEngine,
|
|
2466
|
+
FileSyncStateStore,
|
|
2467
|
+
startServe
|
|
2468
|
+
};
|
|
2469
|
+
//# sourceMappingURL=chunk-MNOPLM7M.js.map
|