leafrelay 0.2.7 → 0.2.9

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.
@@ -1,3333 +0,0 @@
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 DiffMatchPatch2 from "diff-match-patch";
22
- import { createHash as createHash2 } from "crypto";
23
- import { diff3Merge } from "node-diff3";
24
- import { randomUUID } from "crypto";
25
- import { mkdir as mkdir3, readFile as readFile4, readdir as readdir2, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
26
- import { dirname as dirname3, join as join2, resolve as resolve4 } from "path";
27
- var PACKET_TYPES = [
28
- "disconnect",
29
- "connect",
30
- "heartbeat",
31
- "message",
32
- "json",
33
- "event",
34
- "ack",
35
- "error",
36
- "noop"
37
- ];
38
- function encodePacket(packet) {
39
- const type = PACKET_TYPES.indexOf(packet.type ?? "noop");
40
- const id = packet.id ?? "";
41
- let data;
42
- switch (packet.type) {
43
- case "event":
44
- data = JSON.stringify({ name: packet.name, args: packet.args ?? [] });
45
- break;
46
- case "ack":
47
- data = `${packet.ackId ?? ""}${packet.args?.length ? `+${JSON.stringify(packet.args)}` : ""}`;
48
- break;
49
- case "message":
50
- data = String(packet.data ?? "");
51
- break;
52
- case "json":
53
- data = JSON.stringify(packet.data);
54
- break;
55
- default:
56
- break;
57
- }
58
- const fields = [String(type), `${id}${packet.ack === "data" ? "+" : ""}`, packet.endpoint ?? ""];
59
- if (data !== void 0) {
60
- fields.push(data);
61
- }
62
- return fields.join(":");
63
- }
64
- function decodePacket(value) {
65
- const match = value.match(/([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/);
66
- if (!match) {
67
- return {};
68
- }
69
- const packet = {
70
- type: PACKET_TYPES[Number(match[1])],
71
- endpoint: match[4] || ""
72
- };
73
- if (match[2]) {
74
- packet.id = match[2];
75
- packet.ack = match[3] ? "data" : true;
76
- }
77
- const data = match[5] || "";
78
- switch (packet.type) {
79
- case "event": {
80
- const event = JSON.parse(data);
81
- packet.name = event.name;
82
- packet.args = event.args ?? [];
83
- break;
84
- }
85
- case "ack": {
86
- const ack = data.match(/^([0-9]+)(\+)?([\s\S]*)/);
87
- if (ack) {
88
- packet.ackId = ack[1];
89
- packet.args = ack[3] ? JSON.parse(ack[3]) : [];
90
- }
91
- break;
92
- }
93
- case "error": {
94
- const [reason, advice] = data.split("+");
95
- packet.reason = ["transport not supported", "client not handshaken", "unauthorized"][Number(reason)] ?? "";
96
- packet.advice = ["reconnect"][Number(advice)] ?? "";
97
- break;
98
- }
99
- case "json":
100
- packet.data = JSON.parse(data);
101
- break;
102
- case "message":
103
- packet.data = data;
104
- break;
105
- default:
106
- break;
107
- }
108
- return packet;
109
- }
110
- function decodePayload(value) {
111
- if (!value.startsWith("\uFFFD")) {
112
- return [decodePacket(value)];
113
- }
114
- const packets = [];
115
- let offset = 1;
116
- while (offset < value.length) {
117
- const separator = value.indexOf("\uFFFD", offset);
118
- if (separator === -1) {
119
- break;
120
- }
121
- const length = Number(value.slice(offset, separator));
122
- const start = separator + 1;
123
- packets.push(decodePacket(value.slice(start, start + length)));
124
- offset = start + length + 1;
125
- }
126
- return packets;
127
- }
128
- function mergeCookies(current, additions) {
129
- const cookies = /* @__PURE__ */ new Map();
130
- for (const value of [current, ...additions.map((cookie) => cookie.split(";", 1)[0])]) {
131
- for (const part of value.split(";")) {
132
- const separator = part.indexOf("=");
133
- if (separator === -1) {
134
- continue;
135
- }
136
- cookies.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
137
- }
138
- }
139
- return [...cookies].map(([name, value]) => `${name}=${value}`).join("; ");
140
- }
141
- var OverleafRealtimeSocket = class {
142
- constructor(url, options) {
143
- this.options = options;
144
- this.endpoint = new URL(url);
145
- this.origin = options.origin;
146
- this.cookie = options.cookie;
147
- this.reconnectEnabled = options.reconnect ?? true;
148
- void this.connect();
149
- }
150
- options;
151
- events = new EventEmitter();
152
- endpoint;
153
- origin;
154
- cookie;
155
- socket;
156
- connected = false;
157
- projectJoined = false;
158
- explicitlyDisconnected = false;
159
- reconnectEnabled;
160
- reconnectAttempts = 0;
161
- reconnectTimer;
162
- inactivityTimer;
163
- connectionAbort;
164
- closeTimeoutMs = 6e4;
165
- acknowledgementId = 0;
166
- acknowledgements = /* @__PURE__ */ new Map();
167
- io = {
168
- reconnect: (enabled) => {
169
- this.reconnectEnabled = enabled;
170
- if (!enabled && this.reconnectTimer) {
171
- clearTimeout(this.reconnectTimer);
172
- this.reconnectTimer = void 0;
173
- }
174
- }
175
- };
176
- on(event, handler) {
177
- this.events.on(event, handler);
178
- return this;
179
- }
180
- once(event, handler) {
181
- this.events.once(event, handler);
182
- return this;
183
- }
184
- removeListener(event, handler) {
185
- this.events.removeListener(event, handler);
186
- return this;
187
- }
188
- removeAllListeners(event) {
189
- this.events.removeAllListeners(event);
190
- return this;
191
- }
192
- emit(event, ...args) {
193
- const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
194
- const id = callback ? String(++this.acknowledgementId) : void 0;
195
- if (id && callback) {
196
- this.acknowledgements.set(id, callback);
197
- }
198
- const packet = encodePacket({
199
- type: "event",
200
- id,
201
- ack: callback ? "data" : void 0,
202
- name: event,
203
- args
204
- });
205
- if (!this.sendApplicationEvent(packet) && callback) {
206
- if (id) {
207
- this.acknowledgements.delete(id);
208
- }
209
- queueMicrotask(() => callback(new Error("The Overleaf project session is not connected.")));
210
- }
211
- return this;
212
- }
213
- markProjectJoined() {
214
- this.projectJoined = true;
215
- }
216
- disconnect() {
217
- this.explicitlyDisconnected = true;
218
- this.reconnectEnabled = false;
219
- if (this.reconnectTimer) {
220
- clearTimeout(this.reconnectTimer);
221
- }
222
- if (this.connected) {
223
- this.send(encodePacket({ type: "disconnect" }));
224
- }
225
- this.closeSocket("booted");
226
- return this;
227
- }
228
- async connect() {
229
- if (this.explicitlyDisconnected) {
230
- return;
231
- }
232
- const controller = new AbortController();
233
- this.connectionAbort?.abort();
234
- this.connectionAbort = controller;
235
- try {
236
- this.projectJoined = false;
237
- const handshakeUrl = new URL("/socket.io/1/", this.endpoint.origin);
238
- for (const [name, value] of this.endpoint.searchParams) {
239
- handshakeUrl.searchParams.set(name, value);
240
- }
241
- handshakeUrl.searchParams.set("t", String(Date.now()));
242
- const response = await (this.options.fetch ?? fetch)(handshakeUrl, {
243
- headers: { origin: this.origin, cookie: this.cookie },
244
- redirect: "manual",
245
- signal: AbortSignal.any([controller.signal, AbortSignal.timeout(15e3)])
246
- });
247
- if (!response.ok) {
248
- throw new Error(`Socket.IO handshake failed: HTTP ${response.status} ${await response.text()}`);
249
- }
250
- const setCookies = response.headers.getSetCookie?.() ?? [];
251
- this.cookie = mergeCookies(this.cookie, setCookies);
252
- const [sessionId, heartbeatTimeout, closeTimeout, transports] = (await response.text()).split(":");
253
- if (!sessionId || !transports?.split(",").includes("websocket")) {
254
- throw new Error("Overleaf did not offer a Socket.IO WebSocket transport.");
255
- }
256
- this.closeTimeoutMs = Math.max(Number(heartbeatTimeout), Number(closeTimeout), 10) * 1e3;
257
- const websocketUrl = new URL(`/socket.io/1/websocket/${sessionId}`, this.endpoint.origin);
258
- websocketUrl.protocol = websocketUrl.protocol === "https:" ? "wss:" : "ws:";
259
- websocketUrl.search = this.endpoint.search;
260
- if (controller.signal.aborted) {
261
- return;
262
- }
263
- const socket = new WebSocket(websocketUrl, {
264
- headers: { origin: this.origin, cookie: this.cookie }
265
- });
266
- this.socket = socket;
267
- socket.addEventListener("open", () => {
268
- if (this.socket !== socket) {
269
- return;
270
- }
271
- this.resetInactivityTimer();
272
- this.events.emit("connecting", "websocket");
273
- });
274
- socket.addEventListener("message", (event) => {
275
- if (this.socket === socket) {
276
- void this.receive(event.data, socket);
277
- }
278
- });
279
- socket.addEventListener("error", (error2) => {
280
- if (this.socket === socket) {
281
- this.events.emit("error", error2);
282
- }
283
- });
284
- socket.addEventListener("close", (event) => {
285
- if (this.socket === socket) {
286
- this.handleClose(event);
287
- }
288
- });
289
- } catch (error2) {
290
- if (controller.signal.aborted) {
291
- return;
292
- }
293
- this.events.emit("connect_failed", error2);
294
- this.events.emit("error", error2);
295
- this.scheduleReconnect();
296
- }
297
- }
298
- async receive(data, socket) {
299
- this.resetInactivityTimer();
300
- const value = typeof data === "string" ? data : data instanceof Blob ? await data.text() : Buffer.from(data).toString("utf8");
301
- if (this.socket !== socket) {
302
- return;
303
- }
304
- for (const packet of decodePayload(value)) {
305
- this.handlePacket(packet);
306
- }
307
- }
308
- handlePacket(packet) {
309
- switch (packet.type) {
310
- case "connect": {
311
- const wasReconnecting = this.reconnectAttempts > 0;
312
- this.connected = true;
313
- this.events.emit("connect");
314
- if (wasReconnecting) {
315
- this.events.emit("reconnect", "websocket", this.reconnectAttempts);
316
- this.reconnectAttempts = 0;
317
- }
318
- break;
319
- }
320
- case "heartbeat":
321
- this.send(encodePacket({ type: "heartbeat" }));
322
- break;
323
- case "event": {
324
- const args = [...packet.args ?? []];
325
- if (packet.ack === "data" && packet.id) {
326
- args.push((...acknowledgement) => {
327
- this.send(encodePacket({ type: "ack", ackId: packet.id, args: acknowledgement }));
328
- });
329
- }
330
- this.events.emit(packet.name ?? "", ...args);
331
- break;
332
- }
333
- case "ack": {
334
- const callback = packet.ackId && this.acknowledgements.get(packet.ackId);
335
- if (callback && packet.ackId) {
336
- this.acknowledgements.delete(packet.ackId);
337
- callback(...packet.args ?? []);
338
- }
339
- break;
340
- }
341
- case "disconnect":
342
- this.closeSocket("booted");
343
- break;
344
- case "error":
345
- this.events.emit("error", packet.reason || "Socket.IO protocol error");
346
- if (packet.advice === "reconnect") {
347
- this.closeSocket("server error");
348
- }
349
- break;
350
- case "message":
351
- case "json":
352
- this.events.emit("message", packet.data);
353
- break;
354
- default:
355
- break;
356
- }
357
- }
358
- send(packet) {
359
- if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
360
- this.socket.send(packet);
361
- }
362
- }
363
- sendApplicationEvent(packet) {
364
- if (!this.connected || !this.projectJoined || this.socket?.readyState !== WebSocket.OPEN) {
365
- return false;
366
- }
367
- this.socket.send(packet);
368
- return true;
369
- }
370
- resetInactivityTimer() {
371
- if (this.inactivityTimer) {
372
- clearTimeout(this.inactivityTimer);
373
- }
374
- this.inactivityTimer = setTimeout(() => this.closeSocket("timeout"), this.closeTimeoutMs);
375
- }
376
- handleClose(event) {
377
- const wasConnected = this.connected;
378
- this.socket = void 0;
379
- this.connected = false;
380
- this.projectJoined = false;
381
- if (this.inactivityTimer) {
382
- clearTimeout(this.inactivityTimer);
383
- }
384
- if (wasConnected) {
385
- const detail = event.reason ? `: ${event.reason}` : "";
386
- const reason = `transport close (code ${event.code})${detail}`;
387
- this.failAcknowledgements(reason);
388
- this.events.emit("disconnect", reason);
389
- }
390
- this.scheduleReconnect();
391
- }
392
- closeSocket(reason) {
393
- this.connectionAbort?.abort();
394
- this.connectionAbort = void 0;
395
- const wasConnected = this.connected;
396
- this.connected = false;
397
- this.projectJoined = false;
398
- if (this.inactivityTimer) {
399
- clearTimeout(this.inactivityTimer);
400
- }
401
- const socket = this.socket;
402
- this.socket = void 0;
403
- socket?.close();
404
- if (wasConnected) {
405
- this.failAcknowledgements(reason);
406
- this.events.emit("disconnect", reason);
407
- }
408
- if (!this.explicitlyDisconnected) {
409
- this.scheduleReconnect();
410
- }
411
- }
412
- failAcknowledgements(reason) {
413
- const error2 = new Error(`Overleaf realtime connection closed: ${reason}`);
414
- for (const callback of this.acknowledgements.values()) {
415
- callback(error2);
416
- }
417
- this.acknowledgements.clear();
418
- }
419
- scheduleReconnect() {
420
- if (!this.reconnectEnabled || this.explicitlyDisconnected || this.reconnectTimer) {
421
- return;
422
- }
423
- const maxAttempts = this.options.maxReconnectionAttempts ?? Infinity;
424
- if (this.reconnectAttempts >= maxAttempts) {
425
- this.events.emit("reconnect_failed");
426
- return;
427
- }
428
- this.reconnectAttempts += 1;
429
- const initial = this.options.reconnectionDelay ?? 1e3;
430
- const limit = this.options.reconnectionLimit ?? 16e3;
431
- const delay = Math.min(initial * 2 ** (this.reconnectAttempts - 1), limit);
432
- this.events.emit("reconnecting", delay, this.reconnectAttempts);
433
- this.reconnectTimer = setTimeout(() => {
434
- this.reconnectTimer = void 0;
435
- void this.connect();
436
- }, delay);
437
- }
438
- };
439
- var sink;
440
- function setLogSink(nextSink) {
441
- sink = nextSink;
442
- }
443
- function write(level, args) {
444
- if (sink !== void 0) {
445
- sink(level, args);
446
- return;
447
- }
448
- const method = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
449
- method(...args);
450
- }
451
- function log(...args) {
452
- write("info", args);
453
- }
454
- function error(...args) {
455
- write("error", args);
456
- }
457
- function retryAfterMilliseconds(response, now) {
458
- const retryAfter = response.headers.get("retry-after");
459
- if (retryAfter) {
460
- const seconds = Number(retryAfter);
461
- if (Number.isFinite(seconds)) {
462
- return Math.max(1e3, seconds * 1e3);
463
- }
464
- const retryAt = Date.parse(retryAfter);
465
- if (Number.isFinite(retryAt)) {
466
- return Math.max(1e3, retryAt - now);
467
- }
468
- }
469
- return 5e3;
470
- }
471
- var HttpRequestScheduler = class {
472
- constructor(options = {}) {
473
- this.options = options;
474
- this.minimumIntervalMs = options.minimumIntervalMs ?? 300;
475
- this.historyIntervalMs = options.historyIntervalMs ?? 3e3;
476
- this.fetchImplementation = options.fetch ?? fetch;
477
- this.now = options.now ?? Date.now;
478
- this.wait = options.wait ?? ((milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds)));
479
- }
480
- options;
481
- chain = Promise.resolve();
482
- nextRequestAt = 0;
483
- historyNextRequestAt = 0;
484
- rateLimitedUntil = 0;
485
- minimumIntervalMs;
486
- historyIntervalMs;
487
- fetchImplementation;
488
- now;
489
- wait;
490
- fetch(input, init, timeoutMs = this.options.requestTimeoutMs ?? 6e4) {
491
- const url = input instanceof Request ? input.url : String(input);
492
- const history = url.includes("/updates?") || url.includes("/filetree/diff?") || url.includes("/diff?");
493
- return this.enqueue(async () => {
494
- const now = this.now();
495
- const readyAt = Math.max(this.nextRequestAt, this.rateLimitedUntil, history ? this.historyNextRequestAt : 0);
496
- if (readyAt > now) {
497
- await this.wait(readyAt - now);
498
- }
499
- const requestAt = this.now();
500
- this.nextRequestAt = requestAt + this.minimumIntervalMs;
501
- if (history) {
502
- this.historyNextRequestAt = requestAt + this.historyIntervalMs;
503
- }
504
- const timeout = AbortSignal.timeout(timeoutMs);
505
- const signals = [timeout, init?.signal, input instanceof Request ? input.signal : void 0].filter((signal) => Boolean(signal));
506
- let response;
507
- try {
508
- response = await this.fetchImplementation(input, { ...init, signal: AbortSignal.any(signals) });
509
- } catch (error2) {
510
- const destination = new URL(url);
511
- error("HTTP request failed", {
512
- method: init?.method ?? (input instanceof Request ? input.method : "GET"),
513
- url: `${destination.origin}${destination.pathname}`,
514
- error: error2
515
- });
516
- throw error2;
517
- }
518
- if (response.status === 429) {
519
- this.rateLimitedUntil = Math.max(
520
- this.rateLimitedUntil,
521
- this.now() + retryAfterMilliseconds(response, this.now())
522
- );
523
- this.options.onRateLimit?.(this.rateLimitedUntil);
524
- log(`HTTP request scheduler entered rate-limit cooldown until ${new Date(this.rateLimitedUntil).toISOString()}.`);
525
- }
526
- return response;
527
- });
528
- }
529
- enqueue(operation) {
530
- const result = this.chain.then(operation);
531
- this.chain = result.then(() => void 0, () => void 0);
532
- return result;
533
- }
534
- };
535
- var sharedHttpRequestScheduler = new HttpRequestScheduler();
536
- function getSetCookie(res) {
537
- if (typeof res.headers?.getSetCookie === "function") {
538
- return res.headers.getSetCookie();
539
- }
540
- const raw = res.headers?.raw?.()?.["set-cookie"];
541
- if (raw) {
542
- return raw;
543
- }
544
- return [];
545
- }
546
- var BaseAPI = class {
547
- constructor(url, scheduler = sharedHttpRequestScheduler) {
548
- this.scheduler = scheduler;
549
- this.url = url;
550
- }
551
- scheduler;
552
- url;
553
- identity;
554
- retryAfterMs(response, attempt) {
555
- const retryAfter = response.headers?.get?.("retry-after");
556
- if (retryAfter !== void 0 && retryAfter !== null) {
557
- const seconds = Number(retryAfter);
558
- if (Number.isFinite(seconds)) {
559
- return Math.max(1e3, seconds * 1e3);
560
- }
561
- const retryAt = Date.parse(retryAfter);
562
- if (Number.isFinite(retryAt)) {
563
- return Math.max(1e3, retryAt - Date.now());
564
- }
565
- }
566
- return 5e3 * Math.pow(2, attempt);
567
- }
568
- async getCsrfToken() {
569
- const res = await this.scheduler.fetch(this.url + "login", {
570
- method: "GET",
571
- redirect: "manual"
572
- });
573
- const body = await res.text();
574
- const match = body.match(/<input.*name="_csrf".*value="([^"]*)">/);
575
- if (!match) {
576
- throw new Error("Failed to get CSRF token.");
577
- } else {
578
- const csrfToken = match[1];
579
- const cookies = getSetCookie(res)[0]?.split(";")[0] ?? "";
580
- return { csrfToken, cookies };
581
- }
582
- }
583
- async getUserId(cookies) {
584
- const res = await this.scheduler.fetch(this.url + "project", {
585
- method: "GET",
586
- redirect: "manual",
587
- headers: {
588
- "Connection": "keep-alive",
589
- "Cookie": cookies
590
- }
591
- });
592
- const body = await res.text();
593
- const userIDMatch = body.match(/<meta\s+name="ol-user_id"\s+content="([^"]*)">/);
594
- const userEmailMatch = body.match(/<meta\s+name="ol-usersEmail"\s+content="([^"]*)">/);
595
- const csrfTokenMatch = body.match(/<meta\s+name="ol-csrfToken"\s+content="([^"]*)">/);
596
- if (userIDMatch !== null && csrfTokenMatch !== null) {
597
- const userId = userIDMatch[1];
598
- const csrfToken = csrfTokenMatch[1];
599
- const userEmail = userEmailMatch ? userEmailMatch[1] : "";
600
- return { userId, userEmail, csrfToken };
601
- } else {
602
- return void 0;
603
- }
604
- }
605
- // Reference: "github:overleaf/overleaf/services/web/frontend/js/ide/connection/ConnectionManager.js#L137"
606
- _initSocketV0(identity, query) {
607
- const url = new URL(this.url).origin + (query ?? "");
608
- return new OverleafRealtimeSocket(url, {
609
- origin: new URL(this.url).origin,
610
- cookie: identity.cookies,
611
- reconnect: true,
612
- reconnectionDelay: 1e3,
613
- reconnectionLimit: 16e3,
614
- maxReconnectionAttempts: 10,
615
- fetch: (input, init) => this.scheduler.fetch(input, init)
616
- });
617
- }
618
- async passportLogin(email, password) {
619
- const identity = await this.getCsrfToken();
620
- const res = await this.scheduler.fetch(this.url + "login", {
621
- method: "POST",
622
- redirect: "manual",
623
- headers: {
624
- "Accept": "*/*",
625
- "Accept-Encoding": "gzip, deflate, br",
626
- "Connection": "keep-alive",
627
- "Content-Type": "application/json",
628
- "Cookie": identity.cookies,
629
- "X-Csrf-Token": identity.csrfToken
630
- },
631
- body: JSON.stringify({ _csrf: identity.csrfToken, email, password })
632
- });
633
- if (res.status === 302) {
634
- const redirect = (await res.text()).match(/Found. Redirecting to (.*)/)[1];
635
- if (redirect === "/project") {
636
- const cookies = getSetCookie(res)[0] ?? "";
637
- return await this.cookiesLogin(cookies);
638
- } else {
639
- return {
640
- type: "error",
641
- message: `Redirecting to /${redirect}`
642
- };
643
- }
644
- } else if (res.status === 200) {
645
- return {
646
- type: "error",
647
- message: (await res.json()).message.message
648
- };
649
- } else if (res.status === 401) {
650
- return {
651
- type: "error",
652
- message: (await res.json()).message.text
653
- };
654
- } else {
655
- return {
656
- type: "error",
657
- message: `${res.status}: ` + await res.text()
658
- };
659
- }
660
- }
661
- async cookiesLogin(cookies) {
662
- const res = await this.getUserId(cookies);
663
- if (res) {
664
- const { userId, userEmail, csrfToken } = res;
665
- const identity = await this.updateCookies({ cookies, csrfToken });
666
- return {
667
- type: "success",
668
- userInfo: { userId, userEmail },
669
- identity
670
- };
671
- } else {
672
- return {
673
- type: "error",
674
- message: "Failed to get User ID."
675
- };
676
- }
677
- }
678
- async updateCookies(identity) {
679
- const res = await this.scheduler.fetch(this.url + "socket.io/socket.io.js", {
680
- method: "GET",
681
- redirect: "manual",
682
- headers: {
683
- "Connection": "keep-alive",
684
- "Cookie": identity.cookies
685
- }
686
- });
687
- const cookies = getSetCookie(res)[0]?.split(";")[0];
688
- if (cookies) {
689
- identity.cookies = `${identity.cookies}; ${cookies}`;
690
- }
691
- return identity;
692
- }
693
- setIdentity(identity) {
694
- this.identity = identity;
695
- return this;
696
- }
697
- /**
698
- * Check if an HTTP error is transient (worth retrying).
699
- * Retries on: 5xx server errors, network errors (fetch failures), and 429 rate limiting.
700
- */
701
- isTransientError(statusCode, errorMessage) {
702
- if (statusCode === void 0) {
703
- return true;
704
- }
705
- if (statusCode >= 500 || statusCode === 429) {
706
- return true;
707
- }
708
- if (errorMessage && (errorMessage.includes("ECONNRESET") || errorMessage.includes("ETIMEDOUT") || errorMessage.includes("ECONNREFUSED") || errorMessage.includes("ENOTFOUND") || errorMessage.includes("socket hang up"))) {
709
- return true;
710
- }
711
- return false;
712
- }
713
- async request(type, route, body, callback, extraHeaders, timeoutMs) {
714
- if (this.identity === void 0) {
715
- return Promise.reject();
716
- }
717
- const MAX_HTTP_RETRIES = 2;
718
- let lastError = {};
719
- for (let attempt = 0; attempt <= MAX_HTTP_RETRIES; attempt++) {
720
- try {
721
- let res = void 0;
722
- switch (type) {
723
- case "GET":
724
- res = await this.scheduler.fetch(this.url + route, {
725
- method: "GET",
726
- redirect: "manual",
727
- headers: {
728
- "Connection": "keep-alive",
729
- "Cookie": this.identity.cookies,
730
- ...extraHeaders
731
- }
732
- }, timeoutMs);
733
- break;
734
- case "POST":
735
- const content_type = body instanceof FormData ? void 0 : { "Content-Type": "application/json" };
736
- const raw_body = body instanceof FormData ? body : JSON.stringify({
737
- _csrf: this.identity.csrfToken,
738
- ...body
739
- });
740
- res = await this.scheduler.fetch(this.url + route, {
741
- method: "POST",
742
- redirect: "manual",
743
- headers: {
744
- "Connection": "keep-alive",
745
- "Cookie": this.identity.cookies,
746
- ...content_type,
747
- ...extraHeaders
748
- },
749
- body: raw_body
750
- }, timeoutMs);
751
- break;
752
- case "PUT":
753
- break;
754
- case "DELETE":
755
- res = await this.scheduler.fetch(this.url + route, {
756
- method: "DELETE",
757
- redirect: "manual",
758
- headers: {
759
- "Connection": "keep-alive",
760
- "Cookie": this.identity.cookies,
761
- "X-Csrf-Token": this.identity.csrfToken,
762
- ...extraHeaders
763
- }
764
- }, timeoutMs);
765
- break;
766
- }
767
- ;
768
- if (res && (res.status === 200 || res.status === 204)) {
769
- const _res = res.status === 200 ? await res.text() : void 0;
770
- const response = callback && callback(_res);
771
- return {
772
- type: "success",
773
- ...response
774
- };
775
- } else if (res && this.isTransientError(res.status) && attempt < MAX_HTTP_RETRIES) {
776
- const delayMs = res.status === 429 ? this.retryAfterMs(res, attempt) : Math.min(1e3 * Math.pow(2, attempt), 4e3);
777
- log(`HTTP ${res.status} on ${route}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_HTTP_RETRIES})`);
778
- lastError = { statusCode: res.status, message: await res.text().catch(() => "") };
779
- await new Promise((r) => setTimeout(r, delayMs));
780
- continue;
781
- } else {
782
- const resOrFallback = res || { status: "undefined", text: async () => "" };
783
- let errorBody = "";
784
- try {
785
- errorBody = await resOrFallback.text();
786
- } catch {
787
- errorBody = "";
788
- }
789
- return {
790
- type: "error",
791
- statusCode: typeof resOrFallback.status === "number" ? resOrFallback.status : void 0,
792
- message: `${resOrFallback.status}: ${errorBody}`
793
- };
794
- }
795
- } catch (err) {
796
- const errMsg = err?.message || String(err);
797
- if (this.isTransientError(void 0, errMsg) && attempt < MAX_HTTP_RETRIES) {
798
- const delayMs = Math.min(1e3 * Math.pow(2, attempt), 4e3);
799
- log(`HTTP fetch error on ${route}: ${errMsg}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_HTTP_RETRIES})`);
800
- await new Promise((r) => setTimeout(r, delayMs));
801
- continue;
802
- }
803
- return {
804
- type: "error",
805
- statusCode: void 0,
806
- message: errMsg
807
- };
808
- }
809
- }
810
- return {
811
- type: "error",
812
- statusCode: lastError.statusCode,
813
- message: lastError.message || `Request failed after ${MAX_HTTP_RETRIES + 1} attempts`
814
- };
815
- }
816
- async download(route) {
817
- if (this.identity === void 0) {
818
- return Promise.reject();
819
- }
820
- let content = [];
821
- while (true) {
822
- const res = await this.scheduler.fetch(this.url + route, {
823
- method: "GET",
824
- redirect: "manual",
825
- headers: {
826
- "Connection": "keep-alive",
827
- "Cookie": this.identity.cookies
828
- }
829
- });
830
- if (res.status === 200) {
831
- content.push(Buffer.from(await res.arrayBuffer()));
832
- break;
833
- } else if (res.status === 206) {
834
- content.push(Buffer.from(await res.arrayBuffer()));
835
- } else {
836
- break;
837
- }
838
- }
839
- ;
840
- return Buffer.concat(content);
841
- }
842
- async logout(identity) {
843
- this.setIdentity(identity);
844
- return this.request("POST", "logout");
845
- }
846
- async userProjectsJson(identity) {
847
- this.setIdentity(identity);
848
- return this.request("GET", "user/projects", void 0, (res) => {
849
- const projects = JSON.parse(res).projects;
850
- projects.forEach((project) => {
851
- project.id = project._id;
852
- delete project._id;
853
- });
854
- return { projects };
855
- });
856
- }
857
- async getProjectsJson(identity) {
858
- this.setIdentity(identity);
859
- return this.request("POST", "api/project", {}, (res) => {
860
- const projects = JSON.parse(res).projects;
861
- return { projects };
862
- });
863
- }
864
- async projectEntitiesJson(identity, projectId) {
865
- this.setIdentity(identity);
866
- return this.request("GET", `project/${projectId}/entities`, void 0, (res) => {
867
- const entities = JSON.parse(res).entities;
868
- return { entities };
869
- });
870
- }
871
- async newProject(identity, projectName, template) {
872
- this.setIdentity(identity);
873
- return this.request("POST", "project/new", { projectName, template }, (res) => {
874
- const message = JSON.parse(res).project_id;
875
- return { message };
876
- });
877
- }
878
- async cloneProject(identity, projectId, projectName) {
879
- this.setIdentity(identity);
880
- return this.request("POST", `project/${projectId}/clone`, { projectName }, (res) => {
881
- const message = JSON.parse(res).project_id;
882
- return { message };
883
- });
884
- }
885
- async renameProject(identity, projectId, newProjectName) {
886
- this.setIdentity(identity);
887
- return this.request("POST", `project/${projectId}/rename`, { newProjectName });
888
- }
889
- async deleteProject(identity, projectId) {
890
- this.setIdentity(identity);
891
- return this.request("DELETE", `project/${projectId}`);
892
- }
893
- async archiveProject(identity, projectId) {
894
- this.setIdentity(identity);
895
- return this.request(
896
- "POST",
897
- `project/${projectId}/archive`,
898
- void 0,
899
- void 0,
900
- { "X-Csrf-Token": identity.csrfToken }
901
- );
902
- }
903
- async unarchiveProject(identity, projectId) {
904
- this.setIdentity(identity);
905
- return this.request("DELETE", `project/${projectId}/archive`);
906
- }
907
- async trashProject(identity, projectId) {
908
- this.setIdentity(identity);
909
- return this.request(
910
- "POST",
911
- `project/${projectId}/trash`,
912
- void 0,
913
- void 0,
914
- { "X-Csrf-Token": identity.csrfToken }
915
- );
916
- }
917
- async untrashProject(identity, projectId) {
918
- this.setIdentity(identity);
919
- return this.request("DELETE", `project/${projectId}/trash`);
920
- }
921
- async getFile(identity, projectId, fileId) {
922
- this.setIdentity(identity);
923
- const content = await this.download(`project/${projectId}/file/${fileId}`);
924
- return {
925
- type: "success",
926
- content: new Uint8Array(content)
927
- };
928
- }
929
- async addDoc(identity, projectId, parentFolderId, filename) {
930
- this.setIdentity(identity);
931
- return this.request("POST", `project/${projectId}/doc`, { parent_folder_id: parentFolderId, name: filename }, (res) => {
932
- const { _id } = JSON.parse(res);
933
- const entity = { _type: "doc", _id, name: filename };
934
- return { entity };
935
- }, { "X-Csrf-Token": identity.csrfToken });
936
- }
937
- async uploadFile(identity, projectId, parentFolderId, filename, fileContent) {
938
- const formData = new FormData();
939
- const mimeType = mime.getType(filename);
940
- formData.append("targetFolderId", parentFolderId);
941
- formData.append("name", filename);
942
- formData.append("type", mimeType ? mimeType : "text/plain");
943
- formData.append("qqfile", new Blob2([Buffer.from(fileContent)], {
944
- type: mimeType ? mimeType : "application/octet-stream"
945
- }), filename);
946
- this.setIdentity(identity);
947
- return this.request("POST", `project/${projectId}/upload?folder_id=${parentFolderId}`, formData, (res) => {
948
- const { success, entity_id, entity_type } = JSON.parse(res);
949
- const entity = { _type: entity_type, _id: entity_id, name: filename };
950
- return { entity };
951
- }, { "X-Csrf-Token": identity.csrfToken });
952
- }
953
- async uploadProject(identity, filename, fileContent) {
954
- const uuid = uuidv4();
955
- const formData = new FormData();
956
- formData.append("qqfile", new Blob2([Buffer.from(fileContent)], {
957
- type: "application/zip"
958
- }), filename);
959
- this.setIdentity(identity);
960
- return this.request("POST", `project/new/upload?_csrf=${identity.csrfToken}&qquuid=${uuid}&qqfilename=${filename}&qqtotalfilesize=${fileContent.length}`, formData, (res) => {
961
- const message = JSON.parse(res).project_id;
962
- return { message };
963
- });
964
- }
965
- async addFolder(identity, projectId, folderName, parentFolderId) {
966
- const body = { name: folderName, parent_folder_id: parentFolderId };
967
- this.setIdentity(identity);
968
- return this.request("POST", `project/${projectId}/folder`, body, (res) => {
969
- const entity = JSON.parse(res);
970
- return { entity };
971
- }, { "X-Csrf-Token": identity.csrfToken });
972
- }
973
- async deleteEntity(identity, projectId, fileType, fileId) {
974
- this.setIdentity(identity);
975
- return this.request("DELETE", `project/${projectId}/${fileType}/${fileId}`);
976
- }
977
- async deleteAuxFiles(identity, projectId) {
978
- this.setIdentity(identity);
979
- return this.request("DELETE", `project/${projectId}/output`);
980
- }
981
- async renameEntity(identity, projectId, entityType, entityId, name) {
982
- this.setIdentity(identity);
983
- return this.request(
984
- "POST",
985
- `project/${projectId}/${entityType}/${entityId}/rename`,
986
- { name },
987
- void 0,
988
- { "X-Csrf-Token": identity.csrfToken }
989
- );
990
- }
991
- async moveEntity(identity, projectId, entityType, entityId, newParentFolderId) {
992
- this.setIdentity(identity);
993
- return this.request(
994
- "POST",
995
- `project/${projectId}/${entityType}/${entityId}/move`,
996
- { folder_id: newParentFolderId },
997
- void 0,
998
- { "X-Csrf-Token": identity.csrfToken }
999
- );
1000
- }
1001
- async compile(identity, projectId, rootResourcePath, draft = false, stopOnFirstError = false) {
1002
- const body = {
1003
- check: "silent",
1004
- draft,
1005
- incrementalCompilesEnabled: true,
1006
- rootResourcePath,
1007
- // file path e.g. "main.tex"
1008
- stopOnFirstError
1009
- };
1010
- this.setIdentity(identity);
1011
- return this.request("POST", `project/${projectId}/compile?auto_compile=true`, body, (res) => {
1012
- const compile = JSON.parse(res);
1013
- return { compile };
1014
- }, { "X-Csrf-Token": identity.csrfToken }, 3e5);
1015
- }
1016
- async stopCompile(identity, projectId) {
1017
- this.setIdentity(identity);
1018
- return this.request("POST", `project/${projectId}/compile/stop`, void 0, void 0, { "X-Csrf-Token": identity.csrfToken });
1019
- }
1020
- async indexAll(identity, projectId) {
1021
- this.setIdentity(identity);
1022
- return this.request("POST", `project/${projectId}/references/indexAll`, { shouldBroadcast: false }, void 0);
1023
- }
1024
- async getMetadata(identity, projectId) {
1025
- this.setIdentity(identity);
1026
- return this.request("GET", `project/${projectId}/metadata`, void 0, (res) => {
1027
- const meta = JSON.parse(res);
1028
- return { meta };
1029
- });
1030
- }
1031
- async proxyRequestToSpellingApi(identity, language, userId, words) {
1032
- const body = {
1033
- language,
1034
- skipLearnedWords: true,
1035
- token: userId,
1036
- words
1037
- };
1038
- this.setIdentity(identity);
1039
- return this.request("POST", "spelling/check", body, (res) => {
1040
- const misspellings = JSON.parse(res).misspellings;
1041
- return { misspellings };
1042
- });
1043
- }
1044
- async spellingControllerLearn(identity, userId, word) {
1045
- const body = {
1046
- token: userId,
1047
- word
1048
- };
1049
- this.setIdentity(identity);
1050
- return this.request("POST", "spelling/learn", body);
1051
- }
1052
- async spellingControllerUnlearn(identity, word) {
1053
- this.setIdentity(identity);
1054
- return this.request("POST", "spelling/unlearn", { word }, void 0, { "X-Csrf-Token": identity.csrfToken });
1055
- }
1056
- async getProjectSettings(identity, projectId) {
1057
- this.setIdentity(identity);
1058
- return this.request("GET", `project/${projectId}`, void 0, (res) => {
1059
- const body = res || "";
1060
- const learnedWordsMatch = /<meta\s+name="ol-learnedWords"\s+data-type="json"\s+content="(\[.*?\])">/.exec(body);
1061
- const learnedWords = learnedWordsMatch !== null ? JSON.parse(learnedWordsMatch[1].replace(/&quot;/g, '"')) : [];
1062
- const languagesMatch = /<meta\s+name="ol-languages"\s+data-type="json"\s+content="(\[.*?\])">/.exec(body);
1063
- const languages = languagesMatch !== null ? JSON.parse(languagesMatch[1].replace(/&quot;/g, '"')) : [];
1064
- languages.length && languages.unshift({ name: "Off", code: "" });
1065
- const compilers = [
1066
- { code: "pdflatex", name: "pdfLaTex" },
1067
- { code: "latex", name: "LaTex" },
1068
- { code: "xelatex", name: "XeLaTex" },
1069
- { code: "lualatex", name: "LuaLaTex" }
1070
- ];
1071
- const settings = { learnedWords, languages, compilers };
1072
- return { settings };
1073
- });
1074
- }
1075
- async updateProjectSettings(identity, projectId, setting) {
1076
- this.setIdentity(identity);
1077
- return this.request("POST", `project/${projectId}/settings`, setting);
1078
- }
1079
- async getFileFromClsi(identity, url, compileGroup, clsiServerId, pdfDownloadDomain) {
1080
- if (pdfDownloadDomain && clsiServerId) {
1081
- const cdnUrl = `${pdfDownloadDomain.replace(/\/+$/, "")}/${url.replace(/^\/+/g, "")}?compileGroup=${encodeURIComponent(compileGroup)}&clsiserverid=${encodeURIComponent(clsiServerId)}&enable_pdf_caching=true`;
1082
- const content2 = await this._downloadAbsolute(cdnUrl, false);
1083
- return { type: "success", content: new Uint8Array(content2) };
1084
- }
1085
- url = url.replace(/^\/+/g, "");
1086
- this.setIdentity(identity);
1087
- const content = await this.download(url);
1088
- return {
1089
- type: "success",
1090
- content: new Uint8Array(content)
1091
- };
1092
- }
1093
- /** Download from an absolute URL, optionally including web frontend cookies. */
1094
- async _downloadAbsolute(absoluteUrl, includeCookies) {
1095
- const headers = {
1096
- "Connection": "keep-alive"
1097
- };
1098
- if (includeCookies && this.identity) {
1099
- headers["Cookie"] = this.identity.cookies;
1100
- }
1101
- let content = [];
1102
- while (true) {
1103
- const res = await this.scheduler.fetch(absoluteUrl, {
1104
- method: "GET",
1105
- redirect: "manual",
1106
- headers
1107
- });
1108
- if (res.status === 200) {
1109
- content.push(Buffer.from(await res.arrayBuffer()));
1110
- break;
1111
- } else if (res.status === 206) {
1112
- content.push(Buffer.from(await res.arrayBuffer()));
1113
- } else {
1114
- break;
1115
- }
1116
- }
1117
- return Buffer.concat(content);
1118
- }
1119
- async proxySyncPdf(identity, projectId, page, h, v, buildId) {
1120
- this.setIdentity(identity);
1121
- const request = `project/${projectId}/sync/pdf?page=${page}&h=${h.toFixed(2)}&v=${v.toFixed(2)}&editorId=${uuidv4()}&buildId=${buildId}`;
1122
- return this.request(
1123
- "GET",
1124
- `project/${projectId}/sync/pdf?page=${page}&h=${h.toFixed(2)}&v=${v.toFixed(2)}&editorId=${uuidv4()}&buildId=${buildId}`,
1125
- void 0,
1126
- (res) => {
1127
- const syncPdf = JSON.parse(res).code[0];
1128
- return { syncPdf };
1129
- }
1130
- );
1131
- }
1132
- async proxySyncCode(identity, projectId, file, line, column, buildId) {
1133
- this.setIdentity(identity);
1134
- return this.request(
1135
- "GET",
1136
- `project/${projectId}/sync/code?file=${file}&line=${line}&column=${column}&editorId=${uuidv4()}&buildId=${buildId}`,
1137
- void 0,
1138
- (res) => {
1139
- const syncCode = JSON.parse(res).pdf;
1140
- return { syncCode };
1141
- }
1142
- );
1143
- }
1144
- async getAllTags(identity) {
1145
- this.setIdentity(identity);
1146
- return this.request("GET", "tag", void 0, (res) => {
1147
- const tags = JSON.parse(res);
1148
- return { tags };
1149
- });
1150
- }
1151
- async createTag(identity, name) {
1152
- this.setIdentity(identity);
1153
- return this.request("POST", "tag", { name }, (res) => {
1154
- const tags = JSON.parse(res);
1155
- return { tags };
1156
- });
1157
- }
1158
- async renameTag(identity, tagId, name) {
1159
- this.setIdentity(identity);
1160
- return this.request("POST", `tag/${tagId}/rename`, { name });
1161
- }
1162
- async deleteTag(identity, tagId) {
1163
- this.setIdentity(identity);
1164
- return this.request("DELETE", `tag/${tagId}`);
1165
- }
1166
- async addProjectToTag(identity, tagId, projectId) {
1167
- this.setIdentity(identity);
1168
- return this.request("POST", `tag/${tagId}/project/${projectId}`);
1169
- }
1170
- async removeProjectFromTag(identity, tagId, projectId) {
1171
- this.setIdentity(identity);
1172
- return this.request("DELETE", `tag/${tagId}/project/${projectId}`);
1173
- }
1174
- async proxyToHistoryApiAndGetUpdates(identity, projectId, before) {
1175
- const beforeQuery = before ? `&before=${before}` : "";
1176
- this.setIdentity(identity);
1177
- return this.request("GET", `project/${projectId}/updates?min_count=10${beforeQuery}`, void 0, (res) => {
1178
- const updates = JSON.parse(res);
1179
- return { updates };
1180
- });
1181
- }
1182
- async proxyToHistoryApiAndGetFileDiff(identity, projectId, pathname, from, to) {
1183
- this.setIdentity(identity);
1184
- return this.request("GET", `project/${projectId}/diff?pathname=${pathname}&from=${from}&to=${to}`, void 0, (res) => {
1185
- const diff = JSON.parse(res);
1186
- return { diff };
1187
- });
1188
- }
1189
- async proxyToHistoryApiAndGetFileTreeDiff(identity, projectId, from, to) {
1190
- this.setIdentity(identity);
1191
- return this.request("GET", `project/${projectId}/filetree/diff?from=${from}&to=${to}`, void 0, (res) => {
1192
- const treeDiff = JSON.parse(res);
1193
- return { treeDiff };
1194
- });
1195
- }
1196
- async downloadZipOfVersion(identity, projectId, version) {
1197
- this.setIdentity(identity);
1198
- const content = await this.download(`project/${projectId}/version/${version}/zip`);
1199
- return {
1200
- type: "success",
1201
- content: new Uint8Array(content)
1202
- };
1203
- }
1204
- async getLabels(identity, projectId) {
1205
- this.setIdentity(identity);
1206
- return this.request("GET", `project/${projectId}/labels`, void 0, (res) => {
1207
- const labels = JSON.parse(res);
1208
- return { labels };
1209
- });
1210
- }
1211
- async createLabel(identity, projectId, comment, version) {
1212
- this.setIdentity(identity);
1213
- return this.request("POST", `project/${projectId}/labels`, { comment, version }, (res) => {
1214
- const labels = [JSON.parse(res)];
1215
- return { labels };
1216
- });
1217
- }
1218
- async deleteLabel(identity, projectId, labelId) {
1219
- this.setIdentity(identity);
1220
- return this.request("DELETE", `project/${projectId}/labels/${labelId}`);
1221
- }
1222
- async getMessages(identity, projectId, limit = 50) {
1223
- this.setIdentity(identity);
1224
- return this.request("GET", `project/${projectId}/messages?limit=${limit}`, void 0, (res) => {
1225
- const messages = JSON.parse(res);
1226
- return { messages };
1227
- }, { "X-Csrf-Token": identity.csrfToken });
1228
- }
1229
- async sendMessage(identity, projectId, client_id, content) {
1230
- this.setIdentity(identity);
1231
- return this.request("POST", `project/${projectId}/messages`, { client_id, content }, void 0, { "X-Csrf-Token": identity.csrfToken });
1232
- }
1233
- };
1234
- var ExtendedBaseAPI = class extends BaseAPI {
1235
- async refreshLinkedFile(identity, project_id, file_id) {
1236
- this.setIdentity(identity);
1237
- return await this.request("POST", `project/${project_id}/linked_file/${file_id}/refresh`, { shouldReindexReferences: false }, (res) => {
1238
- const message = JSON.parse(res).new_file_id;
1239
- return { message };
1240
- }, { "X-Csrf-Token": identity.csrfToken });
1241
- }
1242
- async createLinkedFile(identity, project_id, parent_folder_id, name, provider, data) {
1243
- this.setIdentity(identity);
1244
- return await this.request("POST", `project/${project_id}/linked_file`, { name, parent_folder_id, provider, data }, (res) => {
1245
- const message = JSON.parse(res).new_file_id;
1246
- return { message };
1247
- }, { "X-Csrf-Token": identity.csrfToken });
1248
- }
1249
- };
1250
- var SOCKET_OPERATION_TIMEOUT_MS = 15e3;
1251
- function decodePackedUtf8(text) {
1252
- return Buffer.from(text, "latin1").toString("utf-8");
1253
- }
1254
- var SocketIOAPI = class _SocketIOAPI {
1255
- constructor(url, api, identity, projectId) {
1256
- this.url = url;
1257
- this.api = api;
1258
- this.identity = identity;
1259
- this.projectId = projectId;
1260
- this.scheme = "realtime";
1261
- this.init();
1262
- }
1263
- url;
1264
- api;
1265
- identity;
1266
- projectId;
1267
- static alternativeSocketFactory;
1268
- scheme;
1269
- record;
1270
- joinedProject;
1271
- pendingProjectJoin;
1272
- _handlers = [];
1273
- connectedPublicId;
1274
- socket;
1275
- emit;
1276
- /** Track the scheme used when the socket was last initialized */
1277
- _socketInitScheme;
1278
- static setAlternativeSocketFactory(factory) {
1279
- _SocketIOAPI.alternativeSocketFactory = factory;
1280
- }
1281
- init() {
1282
- if (this.socket) {
1283
- try {
1284
- if (typeof this.socket.removeAllListeners === "function") {
1285
- this.socket.removeAllListeners();
1286
- }
1287
- if (typeof this.socket.disconnect === "function") {
1288
- this.socket.disconnect();
1289
- }
1290
- } catch {
1291
- }
1292
- }
1293
- switch (this.scheme) {
1294
- case "Alt":
1295
- if (!_SocketIOAPI.alternativeSocketFactory) {
1296
- throw new Error("The alternative Overleaf connection is not available in this runtime.");
1297
- }
1298
- this.socket = _SocketIOAPI.alternativeSocketFactory(
1299
- this.url,
1300
- this.api,
1301
- this.identity,
1302
- this.projectId,
1303
- this.record
1304
- );
1305
- break;
1306
- case "realtime":
1307
- this.record = void 0;
1308
- this.joinedProject = void 0;
1309
- this.resetProjectJoin(new Error("The realtime connection was replaced."));
1310
- const query = `?projectId=${this.projectId}&t=${Date.now()}`;
1311
- this.socket = this.api._initSocketV0(this.identity, query);
1312
- break;
1313
- }
1314
- this.socket.emit[promisify.custom] = (event, ...args) => {
1315
- const timeoutPromise = new Promise((_, reject) => {
1316
- setTimeout(() => {
1317
- reject("timeout");
1318
- }, SOCKET_OPERATION_TIMEOUT_MS);
1319
- });
1320
- const waitPromise = new Promise((resolve6, reject) => {
1321
- this.socket.emit(event, ...args, (err, ...data) => {
1322
- if (err) {
1323
- reject(err);
1324
- } else {
1325
- resolve6(data);
1326
- }
1327
- });
1328
- });
1329
- return Promise.race([waitPromise, timeoutPromise]);
1330
- };
1331
- this.emit = promisify(this.socket.emit).bind(this.socket);
1332
- this.initInternalHandlers();
1333
- this.resumeEventHandlers(this._handlers);
1334
- this._socketInitScheme = this.scheme;
1335
- }
1336
- /** Returns true if the socket needs re-initialization (scheme changed, or socket was never init'd) */
1337
- get needsReinit() {
1338
- return this._socketInitScheme !== this.scheme || !this.socket;
1339
- }
1340
- get connectionScheme() {
1341
- return this.scheme;
1342
- }
1343
- initInternalHandlers() {
1344
- this.socket.on("connect", () => {
1345
- log("SocketIOAPI: connected", { scheme: this.scheme, projectId: this.projectId });
1346
- });
1347
- this.socket.on("connect_failed", (connectionError) => {
1348
- log("SocketIOAPI: connect_failed", { scheme: this.scheme, projectId: this.projectId, error: connectionError });
1349
- });
1350
- this.socket.on("disconnect", (reason) => {
1351
- log("SocketIOAPI: disconnect event", { scheme: this.scheme, projectId: this.projectId, reason });
1352
- if (this.scheme === "realtime") {
1353
- this.joinedProject = void 0;
1354
- this.connectedPublicId = void 0;
1355
- this.resetProjectJoin(new Error(`Overleaf realtime connection closed: ${String(reason ?? "unknown")}`));
1356
- }
1357
- });
1358
- this.socket.on("reconnecting", (delay, attempt) => {
1359
- log("SocketIOAPI: reconnecting", { scheme: this.scheme, projectId: this.projectId, delay, attempt });
1360
- });
1361
- this.socket.on("reconnect", (transport, attempts) => {
1362
- log("SocketIOAPI: reconnected", { scheme: this.scheme, projectId: this.projectId, transport, attempts });
1363
- });
1364
- this.socket.on("reconnect_failed", (error2) => {
1365
- log("SocketIOAPI: reconnect_failed", { scheme: this.scheme, projectId: this.projectId, error: error2 });
1366
- });
1367
- this.socket.on("forceDisconnect", (message, delay = 10) => {
1368
- log("SocketIOAPI: forceDisconnect", { message, delay, projectId: this.projectId });
1369
- });
1370
- this.socket.on("connectionRejected", (err) => {
1371
- log("SocketIOAPI: connectionRejected", { scheme: this.scheme, projectId: this.projectId, error: err });
1372
- if (this.socket.io && typeof this.socket.io.reconnect === "function") {
1373
- this.socket.io.reconnect(false);
1374
- }
1375
- });
1376
- this.socket.on("error", (err) => {
1377
- const message = err?.message || String(err);
1378
- error(`Overleaf connection error: ${message}`);
1379
- });
1380
- if (this.scheme === "realtime") {
1381
- this.pendingProjectJoin ??= this.createProjectJoin();
1382
- this.socket.on("joinProjectResponse", (res) => {
1383
- const publicId = res.publicId;
1384
- const project = res.project;
1385
- this.socket.markProjectJoined?.();
1386
- log("SocketIOAPI: project joined", { scheme: this.scheme, projectId: this.projectId, publicId });
1387
- this.connectedPublicId = publicId;
1388
- this.joinedProject = project;
1389
- this.pendingProjectJoin?.resolve(project);
1390
- for (const handler of this._handlers) {
1391
- handler.onProjectJoined?.(project);
1392
- handler.onConnectionAccepted?.(publicId);
1393
- }
1394
- });
1395
- this.socket.on("connectionRejected", (err) => {
1396
- this.pendingProjectJoin?.reject(err?.message || err);
1397
- });
1398
- }
1399
- }
1400
- createProjectJoin() {
1401
- let resolve6;
1402
- let reject;
1403
- const promise = new Promise((resolveProject, rejectProject) => {
1404
- resolve6 = resolveProject;
1405
- reject = rejectProject;
1406
- });
1407
- void promise.catch(() => void 0);
1408
- return { promise, resolve: resolve6, reject };
1409
- }
1410
- resetProjectJoin(reason) {
1411
- if (reason !== void 0) {
1412
- this.pendingProjectJoin?.reject(reason);
1413
- }
1414
- this.pendingProjectJoin = this.createProjectJoin();
1415
- }
1416
- waitForRealtimeProject() {
1417
- return this.joinedProject ? Promise.resolve(this.joinedProject) : this.pendingProjectJoin.promise;
1418
- }
1419
- async emitProjectEvent(event, ...args) {
1420
- if (this.scheme === "realtime") {
1421
- await this.waitForRealtimeProject();
1422
- }
1423
- return this.emit(event, ...args);
1424
- }
1425
- disconnect() {
1426
- this.socket.disconnect();
1427
- }
1428
- get handlers() {
1429
- return this._handlers;
1430
- }
1431
- get isUsingAlternativeConnectionScheme() {
1432
- return this.scheme === "Alt";
1433
- }
1434
- toggleAlternativeConnectionScheme(url, updatedRecord) {
1435
- this.scheme = this.scheme === "Alt" ? "realtime" : "Alt";
1436
- if (updatedRecord) {
1437
- this.url = url;
1438
- this.record = Promise.resolve(updatedRecord);
1439
- }
1440
- }
1441
- resumeEventHandlers(handlers) {
1442
- this._handlers = [];
1443
- handlers.forEach((handler) => {
1444
- this.updateEventHandlers(handler);
1445
- });
1446
- }
1447
- updateEventHandlers(handlers) {
1448
- this._handlers.push(handlers);
1449
- Object.values(handlers).forEach((handler) => {
1450
- switch (handler) {
1451
- case handlers.onFileCreated:
1452
- this.socket.on("reciveNewDoc", (parentFolderId, doc) => {
1453
- handler(parentFolderId, "doc", doc);
1454
- });
1455
- this.socket.on("reciveNewFile", (parentFolderId, file) => {
1456
- handler(parentFolderId, "file", file);
1457
- });
1458
- this.socket.on("reciveNewFolder", (parentFolderId, folder) => {
1459
- handler(parentFolderId, "folder", folder);
1460
- });
1461
- break;
1462
- case handlers.onFileRenamed:
1463
- this.socket.on("reciveEntityRename", (entityId, newName) => {
1464
- handler(entityId, newName);
1465
- });
1466
- break;
1467
- case handlers.onFileRemoved:
1468
- this.socket.on("removeEntity", (entityId) => {
1469
- handler(entityId);
1470
- });
1471
- break;
1472
- case handlers.onFileMoved:
1473
- this.socket.on("reciveEntityMove", (entityId, folderId) => {
1474
- handler(entityId, folderId);
1475
- });
1476
- break;
1477
- case handlers.onFileChanged:
1478
- this.socket.on("otUpdateApplied", (update) => {
1479
- handler(update);
1480
- });
1481
- break;
1482
- case handlers.onDisconnected:
1483
- this.socket.on("disconnect", (reason) => {
1484
- handler(reason);
1485
- });
1486
- break;
1487
- case handlers.onProjectJoined:
1488
- break;
1489
- case handlers.onConnectionAccepted:
1490
- this.socket.on("connectionAccepted", (_, publicId) => {
1491
- handler(publicId);
1492
- });
1493
- if (this.connectedPublicId !== void 0) {
1494
- queueMicrotask(() => handler(this.connectedPublicId));
1495
- }
1496
- break;
1497
- case handlers.onClientUpdated:
1498
- this.socket.on("clientTracking.clientUpdated", (user) => {
1499
- handler(user);
1500
- });
1501
- break;
1502
- case handlers.onClientDisconnected:
1503
- this.socket.on("clientTracking.clientDisconnected", (id) => {
1504
- handler(id);
1505
- });
1506
- break;
1507
- case handlers.onReceivedMessage:
1508
- this.socket.on("new-chat-message", (message) => {
1509
- handler(message);
1510
- });
1511
- break;
1512
- case handlers.onSpellCheckLanguageUpdated:
1513
- this.socket.on("spellCheckLanguageUpdated", (language) => {
1514
- handler(language);
1515
- });
1516
- break;
1517
- case handlers.onCompilerUpdated:
1518
- this.socket.on("compilerUpdated", (compiler) => {
1519
- handler(compiler);
1520
- });
1521
- break;
1522
- case handlers.onRootDocUpdated:
1523
- this.socket.on("rootDocUpdated", (rootDocId) => {
1524
- handler(rootDocId);
1525
- });
1526
- break;
1527
- default:
1528
- break;
1529
- }
1530
- });
1531
- }
1532
- get unSyncFileChanges() {
1533
- return this.scheme === "Alt" ? this.socket.unSyncedChanges : 0;
1534
- }
1535
- async syncFileChanges() {
1536
- if (this.scheme === "Alt") {
1537
- return await this.socket.uploadToVFS();
1538
- }
1539
- }
1540
- /**
1541
- * Reference: services/web/frontend/js/ide/connection/ConnectionManager.js#L427
1542
- * @param {string} projectId - The project id.
1543
- * @returns {Promise}
1544
- */
1545
- async joinProject(project_id) {
1546
- const timeoutPromise = new Promise((_, reject) => {
1547
- setTimeout(() => {
1548
- reject("timeout");
1549
- }, SOCKET_OPERATION_TIMEOUT_MS);
1550
- });
1551
- switch (this.scheme) {
1552
- case "Alt":
1553
- return Promise.race([
1554
- this.emit("joinProject", { project_id }).then((returns) => returns[0]),
1555
- timeoutPromise
1556
- ]);
1557
- case "realtime":
1558
- return Promise.race([this.waitForRealtimeProject(), timeoutPromise]);
1559
- }
1560
- }
1561
- /**
1562
- * Reference: services/web/frontend/js/ide/editor/Document.js#L500
1563
- * @param {string} docId - The document id.
1564
- * @returns {Promise}
1565
- */
1566
- async joinDoc(docId) {
1567
- return this.emitProjectEvent("joinDoc", docId, { encodeRanges: true }).then((returns) => {
1568
- const [docLinesAscii, version, updates, ranges] = returns;
1569
- const docLines = docLinesAscii.map((line) => decodePackedUtf8(line));
1570
- return { docLines, version, updates, ranges };
1571
- });
1572
- }
1573
- /**
1574
- * Reference: services/web/frontend/js/ide/editor/Document.js#L591
1575
- * @param {string} docId - The document id.
1576
- * @returns {Promise}
1577
- */
1578
- async leaveDoc(docId) {
1579
- return this.emitProjectEvent("leaveDoc", docId).then(() => {
1580
- return;
1581
- });
1582
- }
1583
- /**
1584
- * Reference: services/web/frontend/js/ide/editor/ShareJsDocs.js#L78
1585
- * @param {string} docId - The document id.
1586
- * @param {any} update - The changes.
1587
- * @returns {Promise}
1588
- */
1589
- async applyOtUpdate(docId, update) {
1590
- try {
1591
- await this.emitProjectEvent("applyOtUpdate", docId, update);
1592
- } catch (error2) {
1593
- let detail;
1594
- if (error2 instanceof Error) {
1595
- detail = error2.message;
1596
- } else if (typeof error2 === "string") {
1597
- detail = error2;
1598
- } else {
1599
- try {
1600
- detail = JSON.stringify(error2);
1601
- } catch {
1602
- detail = String(error2);
1603
- }
1604
- }
1605
- throw new Error(`Overleaf document update rejected: ${detail}`);
1606
- }
1607
- }
1608
- /**
1609
- * Reference: services/web/frontend/js/ide/online-users/OnlineUserManager.js#L42
1610
- * @returns {Promise}
1611
- */
1612
- async getConnectedUsers() {
1613
- return this.emitProjectEvent("clientTracking.getConnectedUsers").then((returns) => {
1614
- const [connectedUsers] = returns;
1615
- return connectedUsers;
1616
- });
1617
- }
1618
- /**
1619
- * Reference: services/web/frontend/js/ide/online-users/OnlineUserManager.js#L150
1620
- * @param {string} docId - The document id.
1621
- * @returns {Promise}
1622
- */
1623
- async updatePosition(doc_id, row, column) {
1624
- return this.emitProjectEvent("clientTracking.updatePosition", { row, column, doc_id }).then(() => {
1625
- return;
1626
- });
1627
- }
1628
- };
1629
- function configPath(environment = process.env) {
1630
- return environment.LEAFRELAY_CONFIG ? resolve(environment.LEAFRELAY_CONFIG) : join(environment.LEAFRELAY_HOME ? resolve(environment.LEAFRELAY_HOME) : join(homedir(), ".leafrelay"), "config.json");
1631
- }
1632
- function normalizeServerUrl(value) {
1633
- const url = new URL(value.includes("://") ? value : `https://${value}`);
1634
- url.pathname = "/";
1635
- url.search = "";
1636
- url.hash = "";
1637
- return url.href;
1638
- }
1639
- function serverKey(value) {
1640
- return new URL(normalizeServerUrl(value)).host;
1641
- }
1642
- async function readConfig(path = configPath()) {
1643
- try {
1644
- const parsed = JSON.parse(await readFile(path, "utf8"));
1645
- if (parsed.version !== 1 || typeof parsed.servers !== "object" || parsed.servers === null) {
1646
- throw new Error(`Unsupported LeafRelay configuration in ${path}`);
1647
- }
1648
- return parsed;
1649
- } catch (error2) {
1650
- if (error2.code === "ENOENT") {
1651
- return { version: 1, servers: {}, knownServers: {} };
1652
- }
1653
- throw error2;
1654
- }
1655
- }
1656
- async function writeConfig(config, path = configPath()) {
1657
- await mkdir(dirname(path), { recursive: true, mode: 448 });
1658
- await writeFile(path, `${JSON.stringify(config, null, 2)}
1659
- `, { encoding: "utf8", mode: 384 });
1660
- await chmod(path, 384);
1661
- }
1662
- async function saveServerSession(session, path = configPath()) {
1663
- const config = await readConfig(path);
1664
- const key = serverKey(session.url);
1665
- config.servers[key] = session;
1666
- config.knownServers ??= {};
1667
- config.knownServers[key] ??= { name: key, url: session.url };
1668
- await writeConfig(config, path);
1669
- }
1670
- async function saveKnownServer(name, url, path = configPath()) {
1671
- const config = await readConfig(path);
1672
- const key = serverKey(url);
1673
- config.knownServers ??= {};
1674
- config.knownServers[key] = { name, url: normalizeServerUrl(url) };
1675
- await writeConfig(config, path);
1676
- }
1677
- async function removeKnownServer(server, path = configPath()) {
1678
- const config = await readConfig(path);
1679
- const key = serverKey(server);
1680
- delete config.servers[key];
1681
- delete config.knownServers?.[key];
1682
- await writeConfig(config, path);
1683
- }
1684
- async function removeServerSession(server, path = configPath()) {
1685
- const config = await readConfig(path);
1686
- delete config.servers[serverKey(server)];
1687
- await writeConfig(config, path);
1688
- }
1689
- async function getServerSession(server, environment = process.env, path = configPath(environment)) {
1690
- const key = serverKey(server);
1691
- const config = await readConfig(path);
1692
- const stored = config.servers[key];
1693
- const cookie = environment.LEAFRELAY_COOKIE;
1694
- if (!cookie) {
1695
- return stored;
1696
- }
1697
- return {
1698
- url: stored?.url ?? normalizeServerUrl(server),
1699
- userId: stored?.userId ?? "",
1700
- userEmail: stored?.userEmail ?? "",
1701
- identity: { cookies: cookie, csrfToken: stored?.identity.csrfToken ?? "" },
1702
- updatedAt: stored?.updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
1703
- };
1704
- }
1705
- function projectPath(root, path) {
1706
- const target = resolve2(root, path.replace(/^\/+/, ""));
1707
- const relativePath = relative(root, target);
1708
- if (relativePath.startsWith("..") || relativePath === "..") {
1709
- throw new Error(`Path escapes the local project: ${path}`);
1710
- }
1711
- return target;
1712
- }
1713
- function syncPath(root, path) {
1714
- return `/${relative(root, path).split(sep).join("/")}`;
1715
- }
1716
- var NodeReplicaFileSystem = class {
1717
- constructor(root, ignore) {
1718
- this.root = root;
1719
- this.ignore = ignore;
1720
- }
1721
- root;
1722
- ignore;
1723
- watcher;
1724
- async listFiles() {
1725
- const files = [];
1726
- const visit = async (directory) => {
1727
- for (const entry of await readdir(directory, { withFileTypes: true })) {
1728
- const absolute = resolve2(directory, entry.name);
1729
- const path = syncPath(this.root, absolute);
1730
- if (this.ignore(path) || entry.name.startsWith(".") || entry.isSymbolicLink()) {
1731
- continue;
1732
- }
1733
- if (entry.isDirectory()) {
1734
- await visit(absolute);
1735
- } else if (entry.isFile()) {
1736
- files.push(path);
1737
- }
1738
- }
1739
- };
1740
- await visit(this.root);
1741
- return files.sort();
1742
- }
1743
- async read(path) {
1744
- const target = projectPath(this.root, path);
1745
- try {
1746
- const stat = await lstat(target);
1747
- if (!stat.isFile() || stat.isSymbolicLink()) {
1748
- return void 0;
1749
- }
1750
- return new Uint8Array(await readFile2(target));
1751
- } catch (error2) {
1752
- if (error2.code === "ENOENT") {
1753
- return void 0;
1754
- }
1755
- throw error2;
1756
- }
1757
- }
1758
- async write(path, content) {
1759
- const target = projectPath(this.root, path);
1760
- await mkdir2(dirname2(target), { recursive: true });
1761
- await writeFile2(target, content);
1762
- }
1763
- async remove(path) {
1764
- await rm(projectPath(this.root, path), { force: true, recursive: true });
1765
- }
1766
- async watch(onChange) {
1767
- this.watcher = watch(this.root, {
1768
- ignoreInitial: true,
1769
- followSymlinks: false,
1770
- ignored: (path) => {
1771
- const relativePath = relative(this.root, path);
1772
- if (!relativePath) {
1773
- return false;
1774
- }
1775
- const normalized = `/${relativePath.split(sep).join("/")}`;
1776
- return this.ignore(normalized) || normalized.split("/").some((part) => part.startsWith("."));
1777
- },
1778
- awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
1779
- });
1780
- const handler = (path) => onChange(syncPath(this.root, path));
1781
- this.watcher.on("add", handler).on("change", handler).on("unlink", handler);
1782
- await new Promise((resolveReady, reject) => {
1783
- this.watcher.once("ready", resolveReady).once("error", reject);
1784
- });
1785
- return async () => {
1786
- await this.watcher?.close();
1787
- };
1788
- }
1789
- };
1790
- function parseProjectUri(value) {
1791
- const uri = new URL(value);
1792
- const decodedQuery = decodeURIComponent(uri.search.slice(1));
1793
- const query = new URLSearchParams(decodedQuery);
1794
- const projectId = query.get("project");
1795
- if (!projectId) {
1796
- throw new Error("The project URI in .overleaf/settings.json does not contain a project ID.");
1797
- }
1798
- return {
1799
- serverName: uri.host,
1800
- projectId,
1801
- projectName: decodeURIComponent(uri.pathname.split("/").filter(Boolean)[0] ?? ""),
1802
- userId: query.get("user") ?? void 0
1803
- };
1804
- }
1805
- async function readProjectSettings(directory) {
1806
- const settingsPath = resolve3(directory, ".overleaf", "settings.json");
1807
- const raw = JSON.parse(await readFile3(settingsPath, "utf8"));
1808
- if (typeof raw.uri !== "string") {
1809
- throw new Error(`${settingsPath} does not contain a valid project URI.`);
1810
- }
1811
- const parsed = parseProjectUri(raw.uri);
1812
- const serverName = typeof raw.serverName === "string" ? raw.serverName : parsed.serverName;
1813
- const localReplica = raw.localReplica;
1814
- return {
1815
- uri: raw.uri,
1816
- serverName,
1817
- serverUrl: typeof raw.serverUrl === "string" ? raw.serverUrl : `https://${serverName}/`,
1818
- projectId: parsed.projectId,
1819
- projectName: typeof raw.projectName === "string" ? raw.projectName : parsed.projectName,
1820
- userId: parsed.userId,
1821
- localReplica: { settings: localReplica?.settings ?? {} }
1822
- };
1823
- }
1824
- var DEFAULT_IGNORE_PATTERNS = [
1825
- "**/.*",
1826
- "**/.*/**",
1827
- "**/*.aux",
1828
- "**/__latexindent*",
1829
- "**/*.bbl",
1830
- "**/*.bcf",
1831
- "**/*.blg",
1832
- "**/*.fdb_latexmk",
1833
- "**/*.fls",
1834
- "**/*.lof",
1835
- "**/*.log",
1836
- "**/*.lot",
1837
- "**/*.out",
1838
- "**/*.run.xml",
1839
- "**/*.synctex(busy)",
1840
- "**/*.synctex.gz",
1841
- "**/*.toc",
1842
- "**/*.xdv",
1843
- "**/main.pdf",
1844
- "**/output.pdf"
1845
- ];
1846
- function createIgnoreMatcher(patterns) {
1847
- const configured = Array.isArray(patterns) ? patterns.filter((pattern) => typeof pattern === "string") : [];
1848
- const allPatterns = [...DEFAULT_IGNORE_PATTERNS, ...configured];
1849
- return (path) => {
1850
- const normalized = path.replace(/^\/+/, "");
1851
- return normalized.split("/").some((part) => part.startsWith(".")) || allPatterns.some((pattern) => minimatch(normalized, pattern, { dot: true }));
1852
- };
1853
- }
1854
- function normalizePath(path) {
1855
- const normalized = `/${posix.normalize(path).replace(/^\/+/, "")}`;
1856
- if (normalized.includes("/../") || normalized.endsWith("/..")) {
1857
- throw new Error(`Invalid project path: ${path}`);
1858
- }
1859
- return normalized;
1860
- }
1861
- function textContent(content) {
1862
- if (content.includes(0)) {
1863
- return void 0;
1864
- }
1865
- try {
1866
- return new TextDecoder("utf-8", { fatal: true }).decode(content);
1867
- } catch {
1868
- return void 0;
1869
- }
1870
- }
1871
- var RemoteProject = class {
1872
- constructor(url, projectId, cookies, transport) {
1873
- this.url = url;
1874
- this.projectId = projectId;
1875
- this.cookies = cookies;
1876
- this.externalTransport = transport;
1877
- this.api = transport?.api ?? new BaseAPI(url);
1878
- }
1879
- url;
1880
- projectId;
1881
- cookies;
1882
- api;
1883
- identity;
1884
- socket;
1885
- project;
1886
- entries = /* @__PURE__ */ new Map();
1887
- idToPath = /* @__PURE__ */ new Map();
1888
- collaboratorNames = /* @__PURE__ */ new Map();
1889
- changeHandler;
1890
- reconnectHandler;
1891
- externalTransport;
1892
- async connect() {
1893
- if (this.externalTransport) {
1894
- this.identity = this.externalTransport.identity;
1895
- this.socket = this.externalTransport.socket;
1896
- } else {
1897
- const login = await this.api.cookiesLogin(this.cookies);
1898
- if (login.type !== "success" || !login.identity) {
1899
- throw new Error(login.message || `Authentication to ${this.url} failed.`);
1900
- }
1901
- this.identity = login.identity;
1902
- this.socket = new SocketIOAPI(this.url, this.api, this.identity, this.projectId);
1903
- }
1904
- this.project = await this.socket.joinProject(this.projectId);
1905
- this.indexCollaborators();
1906
- this.reindex();
1907
- this.registerEvents();
1908
- }
1909
- disconnect() {
1910
- if (!this.externalTransport) {
1911
- this.socket?.disconnect();
1912
- }
1913
- }
1914
- onChange(handler) {
1915
- this.changeHandler = handler;
1916
- return () => {
1917
- if (this.changeHandler === handler) {
1918
- this.changeHandler = void 0;
1919
- }
1920
- };
1921
- }
1922
- onReconnect(handler) {
1923
- this.reconnectHandler = handler;
1924
- return () => {
1925
- if (this.reconnectHandler === handler) {
1926
- this.reconnectHandler = void 0;
1927
- }
1928
- };
1929
- }
1930
- listEntries() {
1931
- return [...this.entries.values()];
1932
- }
1933
- entry(path) {
1934
- return this.entries.get(normalizePath(path));
1935
- }
1936
- async read(path) {
1937
- const entry = this.requireEntry(path);
1938
- if (entry.type === "doc") {
1939
- const result = await this.socket.joinDoc(entry.entity._id);
1940
- return new TextEncoder().encode(result.docLines.join("\n"));
1941
- }
1942
- if (entry.type === "file") {
1943
- const result = await this.api.getFile(this.identity, this.projectId, entry.entity._id);
1944
- if (result.type === "success" && result.content) {
1945
- return result.content;
1946
- }
1947
- throw new Error(`Could not download ${path}.`);
1948
- }
1949
- throw new Error(`${path} is not a file.`);
1950
- }
1951
- async mkdir(path) {
1952
- const normalized = normalizePath(path);
1953
- if (this.entries.has(normalized)) {
1954
- return;
1955
- }
1956
- const { parent, name } = await this.ensureParent(normalized);
1957
- const result = await this.api.addFolder(this.identity, this.projectId, name, parent._id);
1958
- if (result.type !== "success" || !result.entity) {
1959
- throw new Error(result.message || `Could not create ${normalized}.`);
1960
- }
1961
- const folder = result.entity;
1962
- folder._type = "folder";
1963
- folder.docs ??= [];
1964
- folder.fileRefs ??= [];
1965
- folder.folders ??= [];
1966
- parent.folders.push(folder);
1967
- this.reindex();
1968
- }
1969
- async write(path, content) {
1970
- const normalized = normalizePath(path);
1971
- const existing = this.entries.get(normalized);
1972
- if (existing?.type === "folder") {
1973
- throw new Error(`${normalized} is a directory.`);
1974
- }
1975
- if (existing?.type === "doc") {
1976
- await this.writeDocument(existing.entity, content);
1977
- return;
1978
- }
1979
- if (existing) {
1980
- await this.remove(normalized);
1981
- }
1982
- const { parent, name } = await this.ensureParent(normalized);
1983
- const text = textContent(content);
1984
- if (text !== void 0) {
1985
- const created = await this.api.addDoc(this.identity, this.projectId, parent._id, name);
1986
- if (created.type !== "success" || !created.entity) {
1987
- throw new Error(created.message || `Could not create ${normalized}.`);
1988
- }
1989
- const entity2 = created.entity;
1990
- entity2._type = "doc";
1991
- parent.docs.push(entity2);
1992
- this.reindex();
1993
- if (content.length !== 0) {
1994
- await this.writeDocument(entity2, content);
1995
- }
1996
- return;
1997
- }
1998
- const uploaded = await this.api.uploadFile(this.identity, this.projectId, parent._id, name, content);
1999
- if (uploaded.type !== "success" || !uploaded.entity) {
2000
- throw new Error(uploaded.message || `Could not upload ${normalized}.`);
2001
- }
2002
- const entity = uploaded.entity;
2003
- entity._type = "file";
2004
- parent.fileRefs.push(entity);
2005
- this.reindex();
2006
- }
2007
- async remove(path) {
2008
- const normalized = normalizePath(path);
2009
- const entry = this.requireEntry(normalized);
2010
- if (!entry.parent) {
2011
- throw new Error("The project root cannot be removed.");
2012
- }
2013
- const result = await this.api.deleteEntity(this.identity, this.projectId, entry.type, entry.entity._id);
2014
- if (result.type !== "success") {
2015
- throw new Error(result.message || `Could not remove ${normalized}.`);
2016
- }
2017
- this.removeFromParent(entry);
2018
- this.reindex();
2019
- }
2020
- async getCurrentVersion() {
2021
- const response = await this.api.proxyToHistoryApiAndGetUpdates(this.identity, this.projectId);
2022
- const version = response.updates?.updates.at(0)?.toV ?? 0;
2023
- if (response.type !== "success" || !Number.isInteger(version)) {
2024
- throw new Error(response.message || "Could not determine the current Overleaf project version.");
2025
- }
2026
- return version;
2027
- }
2028
- async getFileTreeDiff(from, to) {
2029
- if (from >= to) {
2030
- return { diff: [] };
2031
- }
2032
- const response = await this.api.proxyToHistoryApiAndGetFileTreeDiff(
2033
- this.identity,
2034
- this.projectId,
2035
- from,
2036
- to
2037
- );
2038
- if (response.type === "success") {
2039
- return response.treeDiff;
2040
- }
2041
- if (response.statusCode === 404) {
2042
- return void 0;
2043
- }
2044
- throw new Error(response.message || "Could not load the Overleaf file-tree history.");
2045
- }
2046
- async writeDocument(entity, content) {
2047
- const text = textContent(content);
2048
- if (text === void 0) {
2049
- const path = this.idToPath.get(entity._id);
2050
- await this.remove(path);
2051
- await this.write(path, content);
2052
- return;
2053
- }
2054
- const remote = await this.socket.joinDoc(entity._id);
2055
- const previous = remote.docLines.join("\n");
2056
- if (previous === text) {
2057
- return;
2058
- }
2059
- let position = 0;
2060
- const operations = [];
2061
- for (const part of new DiffMatchPatch().diff_main(previous, text)) {
2062
- const start = position;
2063
- if (part[0] !== -1) {
2064
- position += part[1].length;
2065
- }
2066
- if (part[0] === 1) {
2067
- operations.push({ p: start, i: part[1] });
2068
- }
2069
- if (part[0] === -1) {
2070
- operations.push({ p: start, d: part[1] });
2071
- }
2072
- }
2073
- const update = {
2074
- doc: entity._id,
2075
- v: remote.version,
2076
- hash: createHash("sha1").update(`blob ${text.length}\0${text}`).digest("hex"),
2077
- op: operations
2078
- };
2079
- await this.socket.applyOtUpdate(entity._id, update);
2080
- }
2081
- async ensureParent(path) {
2082
- const name = posix.basename(path);
2083
- const parentPath = posix.dirname(path);
2084
- if (parentPath !== "/" && !this.entries.has(parentPath)) {
2085
- await this.mkdir(parentPath);
2086
- }
2087
- const parentEntry = parentPath === "/" ? this.entries.get("/") : this.entries.get(parentPath);
2088
- if (!parentEntry || parentEntry.type !== "folder") {
2089
- throw new Error(`Parent directory ${parentPath} does not exist.`);
2090
- }
2091
- return { parent: parentEntry.entity, name };
2092
- }
2093
- requireEntry(path) {
2094
- const normalized = normalizePath(path);
2095
- const entry = this.entries.get(normalized);
2096
- if (!entry) {
2097
- throw new Error(`Remote path not found: ${normalized}`);
2098
- }
2099
- return entry;
2100
- }
2101
- reindex() {
2102
- this.entries.clear();
2103
- this.idToPath.clear();
2104
- const root = this.project.rootFolder[0];
2105
- root._type = "folder";
2106
- this.indexFolder(root, "/");
2107
- }
2108
- indexFolder(folder, path, parent) {
2109
- this.entries.set(path, { path, type: "folder", entity: folder, parent });
2110
- this.idToPath.set(folder._id, path);
2111
- for (const child of folder.folders ?? []) {
2112
- child._type = "folder";
2113
- this.indexFolder(child, normalizePath(posix.join(path, child.name)), folder);
2114
- }
2115
- for (const child of folder.docs ?? []) {
2116
- child._type = "doc";
2117
- const childPath = normalizePath(posix.join(path, child.name));
2118
- this.entries.set(childPath, { path: childPath, type: "doc", entity: child, parent: folder });
2119
- this.idToPath.set(child._id, childPath);
2120
- }
2121
- for (const child of folder.fileRefs ?? []) {
2122
- child._type = "file";
2123
- const childPath = normalizePath(posix.join(path, child.name));
2124
- this.entries.set(childPath, { path: childPath, type: "file", entity: child, parent: folder });
2125
- this.idToPath.set(child._id, childPath);
2126
- }
2127
- }
2128
- folderById(id) {
2129
- const path = this.idToPath.get(id);
2130
- const entry = path ? this.entries.get(path) : void 0;
2131
- return entry?.type === "folder" ? entry.entity : void 0;
2132
- }
2133
- removeFromParent(entry) {
2134
- if (!entry.parent) {
2135
- return;
2136
- }
2137
- const collection = entry.type === "folder" ? entry.parent.folders : entry.type === "doc" ? entry.parent.docs : entry.parent.fileRefs;
2138
- const index = collection.findIndex((item) => item._id === entry.entity._id);
2139
- if (index !== -1) {
2140
- collection.splice(index, 1);
2141
- }
2142
- }
2143
- emitChange(path, previousPath, actor) {
2144
- this.changeHandler?.({ path: normalizePath(path), previousPath, actor });
2145
- }
2146
- indexCollaborators() {
2147
- for (const member of [this.project.owner, ...this.project.members]) {
2148
- if (!member?._id) {
2149
- continue;
2150
- }
2151
- const name = [member.first_name, member.last_name].filter(Boolean).join(" ").trim();
2152
- if (name) {
2153
- this.collaboratorNames.set(member._id, name);
2154
- }
2155
- }
2156
- }
2157
- actor(userId) {
2158
- if (!userId) {
2159
- return void 0;
2160
- }
2161
- const name = this.collaboratorNames.get(userId);
2162
- return name ? `${name} (Overleaf)` : `Overleaf user ${userId}`;
2163
- }
2164
- registerEvents() {
2165
- this.socket.updateEventHandlers({
2166
- onProjectJoined: (project) => {
2167
- this.project = project;
2168
- this.indexCollaborators();
2169
- this.reindex();
2170
- this.reconnectHandler?.();
2171
- },
2172
- onClientUpdated: (user) => {
2173
- if (user.user_id && user.name) {
2174
- this.collaboratorNames.set(user.user_id, user.name);
2175
- }
2176
- },
2177
- onFileCreated: (parentId, type, entity) => {
2178
- const parent = this.folderById(parentId);
2179
- if (!parent || type === "outputs") {
2180
- return;
2181
- }
2182
- if (this.idToPath.has(entity._id)) {
2183
- return;
2184
- }
2185
- entity._type = type;
2186
- if (type === "folder") {
2187
- const folder = entity;
2188
- folder.docs ??= [];
2189
- folder.fileRefs ??= [];
2190
- folder.folders ??= [];
2191
- parent.folders.push(folder);
2192
- } else if (type === "doc") {
2193
- parent.docs.push(entity);
2194
- } else {
2195
- parent.fileRefs.push(entity);
2196
- }
2197
- const parentPath = this.idToPath.get(parentId) ?? "/";
2198
- this.reindex();
2199
- this.emitChange(posix.join(parentPath, entity.name));
2200
- },
2201
- onFileRenamed: (entityId, newName) => {
2202
- const previousPath = this.idToPath.get(entityId);
2203
- if (!previousPath) {
2204
- return;
2205
- }
2206
- this.entries.get(previousPath).entity.name = newName;
2207
- this.reindex();
2208
- this.emitChange(this.idToPath.get(entityId), previousPath);
2209
- },
2210
- onFileRemoved: (entityId) => {
2211
- const previousPath = this.idToPath.get(entityId);
2212
- if (!previousPath) {
2213
- return;
2214
- }
2215
- const entry = this.entries.get(previousPath);
2216
- this.removeFromParent(entry);
2217
- this.reindex();
2218
- this.emitChange(previousPath);
2219
- },
2220
- onFileMoved: (entityId, parentId) => {
2221
- const previousPath = this.idToPath.get(entityId);
2222
- const nextParent = this.folderById(parentId);
2223
- if (!previousPath || !nextParent) {
2224
- return;
2225
- }
2226
- const entry = this.entries.get(previousPath);
2227
- this.removeFromParent(entry);
2228
- if (entry.type === "folder") {
2229
- nextParent.folders.push(entry.entity);
2230
- } else if (entry.type === "doc") {
2231
- nextParent.docs.push(entry.entity);
2232
- } else {
2233
- nextParent.fileRefs.push(entry.entity);
2234
- }
2235
- this.reindex();
2236
- this.emitChange(this.idToPath.get(entityId), previousPath);
2237
- },
2238
- onFileChanged: (update) => {
2239
- const path = this.idToPath.get(update.doc);
2240
- if (path) {
2241
- this.emitChange(path, void 0, this.actor(update.meta?.user_id));
2242
- }
2243
- }
2244
- });
2245
- }
2246
- };
2247
- var decoder = new TextDecoder("utf-8", { fatal: true });
2248
- function decode(content) {
2249
- if (content === void 0) {
2250
- return "";
2251
- }
2252
- if (content.includes(0)) {
2253
- return void 0;
2254
- }
2255
- try {
2256
- return decoder.decode(content);
2257
- } catch {
2258
- return void 0;
2259
- }
2260
- }
2261
- function lineStarts(content) {
2262
- const starts = [0];
2263
- for (let index = 0; index < content.length; index++) {
2264
- if (content[index] === "\n" && index + 1 < content.length) {
2265
- starts.push(index + 1);
2266
- }
2267
- }
2268
- return starts;
2269
- }
2270
- function lineAt(starts, offset) {
2271
- let low = 0;
2272
- let high = starts.length;
2273
- while (low < high) {
2274
- const middle = Math.floor((low + high) / 2);
2275
- if (starts[middle] <= offset) {
2276
- low = middle + 1;
2277
- } else {
2278
- high = middle;
2279
- }
2280
- }
2281
- return Math.max(1, low);
2282
- }
2283
- function toLineRange(content, start, end) {
2284
- const starts = lineStarts(content);
2285
- const safeStart = Math.min(start, Math.max(0, content.length - 1));
2286
- const safeEnd = Math.min(Math.max(start, end - 1), Math.max(0, content.length - 1));
2287
- return { start: lineAt(starts, safeStart), end: lineAt(starts, safeEnd) };
2288
- }
2289
- function mergeRanges(ranges) {
2290
- const merged = [];
2291
- for (const range of ranges.sort((a, b) => a.start - b.start)) {
2292
- const previous = merged.at(-1);
2293
- if (previous && range.start <= previous.end + 1) {
2294
- previous.end = Math.max(previous.end, range.end);
2295
- } else {
2296
- merged.push({ ...range });
2297
- }
2298
- }
2299
- return merged;
2300
- }
2301
- function formatRanges(ranges) {
2302
- const shown = ranges.slice(0, 3).map((range) => range.start === range.end ? String(range.start) : `${range.start}-${range.end}`);
2303
- return `${shown.join(", ")}${ranges.length > shown.length ? ` (+${ranges.length - shown.length} more)` : ""}`;
2304
- }
2305
- function describeContentChange(before, after) {
2306
- if (before && after && Buffer.from(before).equals(Buffer.from(after))) {
2307
- return void 0;
2308
- }
2309
- const oldText = decode(before);
2310
- const newText = decode(after);
2311
- if (oldText === void 0 || newText === void 0) {
2312
- return `binary bytes ${before?.byteLength ?? 0} -> ${after?.byteLength ?? 0}`;
2313
- }
2314
- if (oldText.length === 0 && newText.length === 0) {
2315
- return void 0;
2316
- }
2317
- if (oldText.length === 0) {
2318
- const count = lineStarts(newText).length;
2319
- return `added lines ${count === 1 ? "1" : `1-${count}`}`;
2320
- }
2321
- if (newText.length === 0) {
2322
- const count = lineStarts(oldText).length;
2323
- return `removed lines ${count === 1 ? "1" : `1-${count}`}`;
2324
- }
2325
- const changes = [];
2326
- let oldOffset = 0;
2327
- let newOffset = 0;
2328
- let current;
2329
- const finish = () => {
2330
- if (current) {
2331
- changes.push(current);
2332
- current = void 0;
2333
- }
2334
- };
2335
- for (const [operation, value] of new DiffMatchPatch2().diff_main(oldText, newText)) {
2336
- if (operation === 0) {
2337
- finish();
2338
- oldOffset += value.length;
2339
- newOffset += value.length;
2340
- continue;
2341
- }
2342
- current ??= { oldStart: oldOffset, oldEnd: oldOffset, newStart: newOffset, newEnd: newOffset };
2343
- if (operation === -1) {
2344
- oldOffset += value.length;
2345
- }
2346
- if (operation === 1) {
2347
- newOffset += value.length;
2348
- }
2349
- current.oldEnd = oldOffset;
2350
- current.newEnd = newOffset;
2351
- }
2352
- finish();
2353
- const ranges = mergeRanges(changes.map((change) => change.newEnd > change.newStart ? toLineRange(newText, change.newStart, change.newEnd) : toLineRange(oldText, change.oldStart, change.oldEnd)));
2354
- return ranges.length === 0 ? void 0 : `lines ${formatRanges(ranges)}`;
2355
- }
2356
- var textDecoder = new TextDecoder("utf-8", { fatal: true });
2357
- var textEncoder = new TextEncoder();
2358
- function decodeText(content) {
2359
- if (content.includes(0)) {
2360
- return void 0;
2361
- }
2362
- try {
2363
- return textDecoder.decode(content);
2364
- } catch {
2365
- return void 0;
2366
- }
2367
- }
2368
- function splitLines(content) {
2369
- return content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2370
- }
2371
- function encodeTextBase(content) {
2372
- return decodeText(content);
2373
- }
2374
- function mergeText(baseContent, localContent, remoteContent) {
2375
- const base = decodeText(baseContent);
2376
- const local = decodeText(localContent);
2377
- const remote = decodeText(remoteContent);
2378
- if (base === void 0 || local === void 0 || remote === void 0) {
2379
- return { status: "binary" };
2380
- }
2381
- const regions = diff3Merge(
2382
- splitLines(local),
2383
- splitLines(base),
2384
- splitLines(remote),
2385
- { excludeFalseConflicts: true }
2386
- );
2387
- if (regions.some((region) => region.conflict !== void 0)) {
2388
- return { status: "conflict" };
2389
- }
2390
- return {
2391
- status: "merged",
2392
- content: textEncoder.encode(regions.flatMap((region) => region.ok ?? []).join(""))
2393
- };
2394
- }
2395
- var SYNC_STATE_SCHEMA_VERSION = 1;
2396
- function sha256(content) {
2397
- return createHash2("sha256").update(content).digest("hex");
2398
- }
2399
- function createSyncState(projectUri, remoteVersion, initialized = true) {
2400
- return {
2401
- schemaVersion: SYNC_STATE_SCHEMA_VERSION,
2402
- projectUri,
2403
- remoteVersion,
2404
- initialized,
2405
- files: {},
2406
- textBases: {},
2407
- conflicts: {}
2408
- };
2409
- }
2410
- function updateSyncCheckpoint(state, path, content) {
2411
- state.files[path] = sha256(content);
2412
- const textBase = encodeTextBase(content);
2413
- if (textBase === void 0) {
2414
- delete state.textBases[path];
2415
- } else {
2416
- state.textBases[path] = textBase;
2417
- }
2418
- }
2419
- function removeSyncCheckpoint(state, path) {
2420
- const checkpointPaths = /* @__PURE__ */ new Set([...Object.keys(state.files), ...Object.keys(state.conflicts)]);
2421
- for (const checkpointPath of checkpointPaths) {
2422
- if (checkpointPath === path || checkpointPath.startsWith(`${path}/`)) {
2423
- delete state.files[checkpointPath];
2424
- delete state.textBases[checkpointPath];
2425
- delete state.conflicts[checkpointPath];
2426
- }
2427
- }
2428
- }
2429
- function readTextBase(state, path) {
2430
- const textBase = state?.textBases[path];
2431
- return textBase === void 0 ? void 0 : new TextEncoder().encode(textBase);
2432
- }
2433
- function normalizePath2(path) {
2434
- return `/${posix2.normalize(path).replace(/^\/+/, "")}`;
2435
- }
2436
- function isRemoteFile(entry) {
2437
- return entry?.type === "doc" || entry?.type === "file";
2438
- }
2439
- var SyncEngine = class {
2440
- constructor(options) {
2441
- this.options = options;
2442
- }
2443
- options;
2444
- state;
2445
- queue = Promise.resolve();
2446
- conflicts = /* @__PURE__ */ new Set();
2447
- stopLocalWatch;
2448
- stopRemoteChange;
2449
- stopRemoteReconnect;
2450
- eventTimers = /* @__PURE__ */ new Map();
2451
- stopped = false;
2452
- failedPaths = /* @__PURE__ */ new Map();
2453
- retryTimer;
2454
- retryDelayMs = 3e4;
2455
- pendingRemoteVersion;
2456
- pendingInitialization = false;
2457
- get syncError() {
2458
- if (this.failedPaths.size === 0) {
2459
- return void 0;
2460
- }
2461
- const [path, failure] = this.failedPaths.entries().next().value;
2462
- return `${this.failedPaths.size} path(s) could not synchronize, including ${path}: ${failure.message}. Other paths remain active; failed paths will retry automatically.`;
2463
- }
2464
- async start() {
2465
- this.stopped = false;
2466
- const startup = this.enqueue(() => this.initialize());
2467
- try {
2468
- await startup;
2469
- } catch (error2) {
2470
- await this.stop();
2471
- throw error2;
2472
- }
2473
- }
2474
- async initialize() {
2475
- this.state = await this.options.stateStore.load();
2476
- this.stopLocalWatch = await this.options.local.watch((path) => this.schedule(path, "local"));
2477
- this.stopRemoteChange = this.options.remote.onChange((change) => {
2478
- if (this.stopped) {
2479
- return;
2480
- }
2481
- if (change.previousPath) {
2482
- this.schedule(change.previousPath, "remote", change.actor);
2483
- }
2484
- this.schedule(change.path, "remote", change.actor);
2485
- });
2486
- this.stopRemoteReconnect = this.options.remote.onReconnect?.(() => {
2487
- if (this.stopped) {
2488
- return;
2489
- }
2490
- this.queue = this.queue.then(() => this.reconcileAfterReconnect()).catch((error2) => {
2491
- this.options.log(`Failed to reconcile after reconnecting: ${error2 instanceof Error ? error2.message : String(error2)}`);
2492
- });
2493
- });
2494
- const currentVersion = await this.options.remote.getCurrentVersion();
2495
- const localFiles = await this.options.local.listFiles();
2496
- const remoteFiles = this.remoteFilePaths();
2497
- this.options.log(`Checking replica at Overleaf version ${currentVersion}: ${localFiles.length} local file(s), ${remoteFiles.length} remote file(s).`);
2498
- if (!this.state || !this.state.initialized) {
2499
- await this.initializeState(currentVersion, localFiles, remoteFiles);
2500
- } else {
2501
- this.restoreConflicts();
2502
- await this.startupReconcile(currentVersion, localFiles, remoteFiles);
2503
- }
2504
- this.options.log(this.syncError ? `Synchronization active with pending retries. ${this.syncError}` : `Synchronization active at Overleaf version ${this.state.remoteVersion}.`);
2505
- }
2506
- async stop() {
2507
- this.stopped = true;
2508
- if (this.retryTimer) {
2509
- clearTimeout(this.retryTimer);
2510
- this.retryTimer = void 0;
2511
- }
2512
- this.stopRemoteChange?.();
2513
- this.stopRemoteChange = void 0;
2514
- this.stopRemoteReconnect?.();
2515
- this.stopRemoteReconnect = void 0;
2516
- for (const pending of this.eventTimers.values()) {
2517
- clearTimeout(pending.timer);
2518
- }
2519
- this.eventTimers.clear();
2520
- await this.stopLocalWatch?.();
2521
- this.stopLocalWatch = void 0;
2522
- await this.queue;
2523
- }
2524
- getConflicts() {
2525
- return [...this.conflicts].sort();
2526
- }
2527
- async resolveConflict(path, winner) {
2528
- const normalized = normalizePath2(path);
2529
- if (!this.conflicts.has(normalized)) {
2530
- throw new Error(`No unresolved conflict exists for ${normalized}.`);
2531
- }
2532
- await this.enqueue(async () => {
2533
- const remoteEntry = this.options.remote.entry(normalized);
2534
- if (winner === "local") {
2535
- const localContent = await this.options.local.read(normalized);
2536
- if (localContent) {
2537
- await this.options.remote.write(normalized, localContent);
2538
- const synchronized = await this.options.remote.read(normalized);
2539
- if (sha256(synchronized) !== sha256(localContent)) {
2540
- await this.options.local.write(normalized, synchronized);
2541
- }
2542
- updateSyncCheckpoint(this.state, normalized, synchronized);
2543
- } else {
2544
- if (remoteEntry) {
2545
- await this.options.remote.remove(normalized);
2546
- }
2547
- removeSyncCheckpoint(this.state, normalized);
2548
- }
2549
- } else if (isRemoteFile(remoteEntry)) {
2550
- const remoteContent = await this.options.remote.read(normalized);
2551
- await this.options.local.write(normalized, remoteContent);
2552
- updateSyncCheckpoint(this.state, normalized, remoteContent);
2553
- } else {
2554
- await this.options.local.remove(normalized);
2555
- removeSyncCheckpoint(this.state, normalized);
2556
- }
2557
- this.conflicts.delete(normalized);
2558
- delete this.state.conflicts[normalized];
2559
- await this.options.stateStore.save(this.state);
2560
- this.options.log(`Resolved ${normalized} using ${winner === "local" ? "local" : "Overleaf"} content.`);
2561
- });
2562
- }
2563
- async retry(path) {
2564
- const normalized = normalizePath2(path);
2565
- const reason = this.state.conflicts[normalized];
2566
- this.conflicts.delete(normalized);
2567
- delete this.state.conflicts[normalized];
2568
- try {
2569
- await this.enqueue(() => this.reconcilePath(normalized, "local"));
2570
- } catch (error2) {
2571
- if (reason !== void 0) {
2572
- this.conflicts.add(normalized);
2573
- this.state.conflicts[normalized] = reason;
2574
- await this.options.stateStore.save(this.state);
2575
- }
2576
- throw error2;
2577
- }
2578
- }
2579
- async initializeState(currentVersion, localFiles, remoteFiles) {
2580
- if (!this.state) {
2581
- this.state = createSyncState(this.options.projectUri, currentVersion, false);
2582
- await this.options.stateStore.save(this.state);
2583
- }
2584
- this.restoreConflicts();
2585
- this.pendingRemoteVersion = currentVersion;
2586
- this.pendingInitialization = true;
2587
- const paths = /* @__PURE__ */ new Set([...localFiles, ...remoteFiles, ...Object.keys(this.state.files)]);
2588
- for (const path of [...paths].sort()) {
2589
- const normalized = normalizePath2(path);
2590
- if (this.conflicts.has(normalized)) {
2591
- continue;
2592
- }
2593
- try {
2594
- if (this.state.files[normalized] !== void 0) {
2595
- await this.synchronizePath(normalized, "local");
2596
- } else {
2597
- await this.initializePath(normalized);
2598
- }
2599
- } catch (error2) {
2600
- this.recordFailure(normalized, "local", error2);
2601
- }
2602
- }
2603
- await this.finishRemoteBatch();
2604
- if (this.conflicts.size !== 0) {
2605
- this.options.log(`Synchronization remains active; ${this.conflicts.size} conflicting path(s) are paused.`);
2606
- }
2607
- }
2608
- async initializePath(path) {
2609
- const remoteEntry = this.options.remote.entry(path);
2610
- if (remoteEntry?.type === "folder") {
2611
- return;
2612
- }
2613
- const localContent = await this.options.local.read(path);
2614
- const remoteContent = isRemoteFile(remoteEntry) ? await this.options.remote.read(path) : void 0;
2615
- const localHash = localContent && sha256(localContent);
2616
- const remoteHash = remoteContent && sha256(remoteContent);
2617
- if (localHash === remoteHash) {
2618
- if (localContent) {
2619
- updateSyncCheckpoint(this.state, path, localContent);
2620
- await this.options.stateStore.save(this.state);
2621
- }
2622
- return;
2623
- }
2624
- if (!localContent && remoteContent) {
2625
- await this.options.local.write(path, remoteContent);
2626
- updateSyncCheckpoint(this.state, path, remoteContent);
2627
- await this.options.stateStore.save(this.state);
2628
- this.logChange("pull", "add", path, void 0, remoteContent);
2629
- return;
2630
- }
2631
- await this.recordConflict(
2632
- path,
2633
- remoteContent ? "Local and Overleaf content differ, but no common checkpoint exists." : "The local path has no matching Overleaf file and no common checkpoint exists."
2634
- );
2635
- }
2636
- restoreConflicts() {
2637
- for (const [path, reason] of Object.entries(this.state.conflicts)) {
2638
- if (this.conflicts.has(path)) {
2639
- continue;
2640
- }
2641
- this.conflicts.add(path);
2642
- this.options.onConflict(path, reason);
2643
- }
2644
- }
2645
- async recordConflict(path, reason) {
2646
- this.conflicts.add(path);
2647
- this.state.conflicts[path] = reason;
2648
- await this.options.stateStore.save(this.state);
2649
- this.options.onConflict(path, reason);
2650
- }
2651
- async startupReconcile(currentVersion, localFiles, remoteFiles) {
2652
- const state = this.state;
2653
- if (state.remoteVersion > currentVersion) {
2654
- throw new Error("Synchronization paused: the local checkpoint is newer than the Overleaf project history.");
2655
- }
2656
- const localHashes = /* @__PURE__ */ new Map();
2657
- for (const path of localFiles) {
2658
- const content = await this.options.local.read(path);
2659
- if (content) {
2660
- localHashes.set(path, sha256(content));
2661
- }
2662
- }
2663
- const localChanged = /* @__PURE__ */ new Set();
2664
- for (const path of /* @__PURE__ */ new Set([...Object.keys(state.files), ...localFiles])) {
2665
- if (state.files[path] !== localHashes.get(path)) {
2666
- localChanged.add(path);
2667
- }
2668
- }
2669
- let remoteDiff = { diff: [] };
2670
- if (state.remoteVersion < currentVersion) {
2671
- remoteDiff = await this.options.remote.getFileTreeDiff(state.remoteVersion, currentVersion);
2672
- if (!remoteDiff) {
2673
- throw new Error("Synchronization paused: Overleaf history is unavailable, so remote changes cannot be distinguished safely.");
2674
- }
2675
- }
2676
- const candidates = /* @__PURE__ */ new Set([...localChanged, ...this.failedPaths.keys()]);
2677
- const knownFiles = /* @__PURE__ */ new Set([...Object.keys(state.files), ...localFiles, ...remoteFiles]);
2678
- for (const path of [...localFiles, ...remoteFiles]) {
2679
- if (state.files[path] === void 0) {
2680
- candidates.add(path);
2681
- }
2682
- }
2683
- for (const change of remoteDiff.diff) {
2684
- if (!change.operation) {
2685
- continue;
2686
- }
2687
- const paths = [change.pathname, change.newPathname].filter((path) => Boolean(path));
2688
- for (const changedPath of paths.map(normalizePath2)) {
2689
- let matched = false;
2690
- for (const filePath of knownFiles) {
2691
- if (filePath === changedPath || filePath.startsWith(`${changedPath}/`)) {
2692
- candidates.add(filePath);
2693
- matched = true;
2694
- }
2695
- }
2696
- if (!matched) {
2697
- candidates.add(changedPath);
2698
- }
2699
- }
2700
- }
2701
- this.pendingRemoteVersion = currentVersion;
2702
- this.options.log(`Reconciling ${candidates.size} changed path(s).`);
2703
- for (const path of [...candidates].sort()) {
2704
- if (this.stopped) {
2705
- return;
2706
- }
2707
- try {
2708
- await this.synchronizePath(path, localChanged.has(path) ? "local" : "remote");
2709
- } catch (error2) {
2710
- this.recordFailure(path, localChanged.has(path) ? "local" : "remote", error2);
2711
- }
2712
- }
2713
- await this.finishRemoteBatch();
2714
- if (this.conflicts.size !== 0) {
2715
- this.options.log(`Synchronization remains active; ${this.conflicts.size} conflicting path(s) are paused.`);
2716
- }
2717
- }
2718
- async reconcileAfterReconnect() {
2719
- if (this.stopped) {
2720
- return;
2721
- }
2722
- const currentVersion = await this.options.remote.getCurrentVersion();
2723
- await this.startupReconcile(
2724
- currentVersion,
2725
- await this.options.local.listFiles(),
2726
- this.remoteFilePaths()
2727
- );
2728
- this.options.log(this.syncError ? `Realtime connection restored; ${this.syncError}` : `Realtime connection restored at Overleaf version ${currentVersion}.`);
2729
- }
2730
- schedule(path, source, actor) {
2731
- const normalized = normalizePath2(path);
2732
- if (this.stopped || this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
2733
- return;
2734
- }
2735
- const previous = this.eventTimers.get(normalized);
2736
- if (previous) {
2737
- clearTimeout(previous.timer);
2738
- }
2739
- const actors = previous?.actors ?? /* @__PURE__ */ new Set();
2740
- let unknownRemoteActor = previous?.unknownRemoteActor ?? false;
2741
- if (source === "remote") {
2742
- if (actor) {
2743
- actors.add(actor);
2744
- } else {
2745
- unknownRemoteActor = true;
2746
- }
2747
- }
2748
- const pending = {
2749
- source: previous?.source === "local" || source === "local" ? "local" : "remote",
2750
- actors,
2751
- unknownRemoteActor,
2752
- timer: setTimeout(() => {
2753
- this.eventTimers.delete(normalized);
2754
- if (this.stopped) {
2755
- return;
2756
- }
2757
- const remoteActor = pending.unknownRemoteActor ? void 0 : pending.actors.size === 1 ? [...pending.actors][0] : pending.actors.size > 1 ? "multiple Overleaf collaborators" : void 0;
2758
- this.queue = this.queue.then(() => this.synchronizePath(normalized, pending.source, remoteActor)).catch((error2) => {
2759
- this.recordFailure(normalized, pending.source, error2, remoteActor);
2760
- });
2761
- }, 250)
2762
- };
2763
- this.eventTimers.set(normalized, pending);
2764
- }
2765
- async synchronizePath(path, source, actor) {
2766
- if (this.pendingInitialization && this.state.files[path] === void 0) {
2767
- await this.initializePath(path);
2768
- } else {
2769
- await this.reconcilePath(path, source, actor);
2770
- }
2771
- if (this.failedPaths.delete(path)) {
2772
- this.options.onSyncError?.(this.syncError);
2773
- }
2774
- }
2775
- recordFailure(path, source, error2, actor) {
2776
- const message = error2 instanceof Error ? error2.message : String(error2);
2777
- this.failedPaths.set(path, { source, actor, message });
2778
- this.options.log(`Failed to synchronize ${path}: ${message}; retrying without stopping other paths.`);
2779
- this.options.onSyncError?.(this.syncError);
2780
- this.scheduleRetry();
2781
- }
2782
- scheduleRetry() {
2783
- if (this.stopped || this.retryTimer || this.failedPaths.size === 0) {
2784
- return;
2785
- }
2786
- this.retryTimer = setTimeout(() => {
2787
- this.retryTimer = void 0;
2788
- void this.enqueue(async () => {
2789
- if (this.stopped) {
2790
- return;
2791
- }
2792
- for (const [path, failure] of [...this.failedPaths]) {
2793
- if (this.stopped) {
2794
- return;
2795
- }
2796
- try {
2797
- await this.synchronizePath(path, failure.source, failure.actor);
2798
- this.options.log(`Recovered synchronization for ${path}.`);
2799
- } catch (error2) {
2800
- const message = error2 instanceof Error ? error2.message : String(error2);
2801
- this.failedPaths.set(path, { ...failure, message });
2802
- this.options.log(`Retry failed for ${path}: ${message}`);
2803
- }
2804
- }
2805
- await this.finishRemoteBatch();
2806
- }).catch((error2) => this.options.log(`Could not checkpoint retry batch: ${String(error2)}`)).finally(() => {
2807
- this.retryDelayMs = this.failedPaths.size ? Math.min(this.retryDelayMs * 2, 12e4) : 3e4;
2808
- this.scheduleRetry();
2809
- });
2810
- }, this.retryDelayMs);
2811
- }
2812
- async finishRemoteBatch() {
2813
- if (this.stopped || this.failedPaths.size || this.pendingRemoteVersion === void 0) {
2814
- return;
2815
- }
2816
- this.state.remoteVersion = this.pendingRemoteVersion;
2817
- if (this.pendingInitialization) {
2818
- this.state.initialized = true;
2819
- }
2820
- await this.options.stateStore.save(this.state);
2821
- this.pendingRemoteVersion = void 0;
2822
- this.pendingInitialization = false;
2823
- }
2824
- enqueue(operation) {
2825
- const result = this.queue.then(operation);
2826
- this.queue = result.then(() => void 0, () => void 0);
2827
- return result;
2828
- }
2829
- async reconcilePath(path, source, actor) {
2830
- const normalized = normalizePath2(path);
2831
- if (this.stopped || this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
2832
- return;
2833
- }
2834
- const remoteEntry = this.options.remote.entry(normalized);
2835
- if (remoteEntry?.type === "folder") {
2836
- return;
2837
- }
2838
- const localContent = await this.options.local.read(normalized);
2839
- const remoteContent = isRemoteFile(remoteEntry) ? await this.options.remote.read(normalized) : void 0;
2840
- const baseHash = this.state.files[normalized];
2841
- const baseContent = readTextBase(this.state, normalized);
2842
- const localHash = localContent && sha256(localContent);
2843
- const remoteHash = remoteContent && sha256(remoteContent);
2844
- if (localHash === remoteHash) {
2845
- if (localHash !== baseHash) {
2846
- if (localContent) {
2847
- updateSyncCheckpoint(this.state, normalized, localContent);
2848
- } else {
2849
- removeSyncCheckpoint(this.state, normalized);
2850
- }
2851
- await this.options.stateStore.save(this.state);
2852
- }
2853
- return;
2854
- }
2855
- const localChanged = localHash !== baseHash;
2856
- const remoteChanged = remoteHash !== baseHash;
2857
- if (localChanged && remoteChanged) {
2858
- if (baseContent && localContent && remoteContent) {
2859
- const merged = mergeText(baseContent, localContent, remoteContent);
2860
- if (merged.status === "merged" && merged.content) {
2861
- await this.options.local.write(normalized, merged.content);
2862
- await this.options.remote.write(normalized, merged.content);
2863
- updateSyncCheckpoint(this.state, normalized, merged.content);
2864
- await this.options.stateStore.save(this.state);
2865
- this.logChange("merge", "update", normalized, baseContent, merged.content, actor);
2866
- return;
2867
- }
2868
- }
2869
- const reason = "Both local and Overleaf content changed since the last synchronized checkpoint.";
2870
- await this.recordConflict(normalized, reason);
2871
- return;
2872
- }
2873
- if (source === "remote" && localChanged && !remoteChanged) {
2874
- return;
2875
- }
2876
- if (localChanged || !remoteChanged && source === "local") {
2877
- let pushedContent = localContent;
2878
- if (!localContent) {
2879
- if (remoteEntry) {
2880
- await this.options.remote.remove(normalized);
2881
- }
2882
- removeSyncCheckpoint(this.state, normalized);
2883
- } else {
2884
- await this.options.remote.write(normalized, localContent);
2885
- const synchronized = await this.options.remote.read(normalized);
2886
- if (sha256(synchronized) !== sha256(localContent)) {
2887
- await this.options.local.write(normalized, synchronized);
2888
- }
2889
- pushedContent = synchronized;
2890
- updateSyncCheckpoint(this.state, normalized, synchronized);
2891
- }
2892
- await this.options.stateStore.save(this.state);
2893
- if (!localContent && remoteEntry) {
2894
- this.logChange("push", "delete", normalized, baseContent ?? remoteContent, void 0, actor);
2895
- } else if (localContent) {
2896
- this.logChange(
2897
- "push",
2898
- remoteEntry ? "update" : "add",
2899
- normalized,
2900
- baseContent ?? remoteContent,
2901
- pushedContent,
2902
- actor
2903
- );
2904
- }
2905
- return;
2906
- }
2907
- if (!remoteContent) {
2908
- if (localContent) {
2909
- await this.options.local.remove(normalized);
2910
- }
2911
- removeSyncCheckpoint(this.state, normalized);
2912
- } else {
2913
- await this.options.local.write(normalized, remoteContent);
2914
- updateSyncCheckpoint(this.state, normalized, remoteContent);
2915
- }
2916
- await this.options.stateStore.save(this.state);
2917
- if (!remoteContent && localContent) {
2918
- this.logChange("pull", "delete", normalized, baseContent ?? localContent, void 0, actor);
2919
- } else if (remoteContent) {
2920
- this.logChange(
2921
- "pull",
2922
- localContent ? "update" : "add",
2923
- normalized,
2924
- baseContent ?? localContent,
2925
- remoteContent,
2926
- actor
2927
- );
2928
- }
2929
- }
2930
- logChange(direction, operation, path, before, after, actor) {
2931
- const summary = describeContentChange(before, after);
2932
- const source = direction === "push" ? "local filesystem" : direction === "pull" ? actor ?? "Overleaf" : `local filesystem + ${actor ?? "Overleaf"}`;
2933
- this.options.log([
2934
- `[${direction}] ${operation} ${JSON.stringify(path)}`,
2935
- summary,
2936
- `source=${source}`
2937
- ].filter(Boolean).join("; "));
2938
- }
2939
- remoteFilePaths() {
2940
- return this.options.remote.listEntries().filter((entry) => isRemoteFile(entry) && !this.shouldIgnore(entry.path)).map((entry) => entry.path);
2941
- }
2942
- shouldIgnore(path) {
2943
- return this.options.ignore(path) || path.split("/").some((part) => part.startsWith("."));
2944
- }
2945
- };
2946
- var HASH_PATTERN = /^[a-f0-9]{64}$/;
2947
- var encoder = new TextEncoder();
2948
- function json(value) {
2949
- return `${JSON.stringify(value, null, 2)}
2950
- `;
2951
- }
2952
- function pathKey(path) {
2953
- return sha256(encoder.encode(path));
2954
- }
2955
- async function readJson(path) {
2956
- return JSON.parse(await readFile4(path, "utf8"));
2957
- }
2958
- async function listFiles(directory) {
2959
- const result = [];
2960
- const visit = async (current) => {
2961
- let entries;
2962
- try {
2963
- entries = await readdir2(current, { withFileTypes: true });
2964
- } catch (error2) {
2965
- if (error2.code === "ENOENT") {
2966
- return;
2967
- }
2968
- throw error2;
2969
- }
2970
- for (const entry of entries) {
2971
- const path = join2(current, entry.name);
2972
- if (entry.isDirectory()) {
2973
- await visit(path);
2974
- } else if (entry.isFile()) {
2975
- result.push(path);
2976
- }
2977
- }
2978
- };
2979
- await visit(directory);
2980
- return result;
2981
- }
2982
- async function atomicWrite(path, content) {
2983
- await mkdir3(dirname3(path), { recursive: true });
2984
- const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
2985
- try {
2986
- await writeFile3(temporary, content);
2987
- await rename(temporary, path);
2988
- } finally {
2989
- await rm2(temporary, { force: true });
2990
- }
2991
- }
2992
- function parseFormat(value, projectUri) {
2993
- if (typeof value !== "object" || value === null) {
2994
- return false;
2995
- }
2996
- const format = value;
2997
- return format.schemaVersion === SYNC_STATE_SCHEMA_VERSION && format.projectUri === projectUri;
2998
- }
2999
- function parseCursor(value) {
3000
- if (typeof value !== "object" || value === null) {
3001
- return false;
3002
- }
3003
- const cursor = value;
3004
- return Number.isInteger(cursor.remoteVersion) && cursor.remoteVersion >= 0 && typeof cursor.initialized === "boolean";
3005
- }
3006
- function parsePathRecord(value) {
3007
- if (typeof value !== "object" || value === null) {
3008
- return false;
3009
- }
3010
- const record = value;
3011
- if (typeof record.path !== "string" || !record.path.startsWith("/")) {
3012
- return false;
3013
- }
3014
- if (record.hash !== void 0 && (typeof record.hash !== "string" || !HASH_PATTERN.test(record.hash))) {
3015
- return false;
3016
- }
3017
- if (record.baseHash !== void 0 && (typeof record.baseHash !== "string" || !HASH_PATTERN.test(record.baseHash) || record.hash !== record.baseHash)) {
3018
- return false;
3019
- }
3020
- if (record.conflict !== void 0 && typeof record.conflict !== "string") {
3021
- return false;
3022
- }
3023
- return record.hash !== void 0 || record.conflict !== void 0;
3024
- }
3025
- function createRecord(state, path) {
3026
- const hash = state.files[path];
3027
- const textBase = state.textBases[path];
3028
- const conflict = state.conflicts[path];
3029
- if (hash === void 0 && conflict === void 0) {
3030
- return void 0;
3031
- }
3032
- return {
3033
- path,
3034
- ...hash === void 0 ? {} : { hash },
3035
- ...textBase === void 0 ? {} : { baseHash: hash },
3036
- ...conflict === void 0 ? {} : { conflict }
3037
- };
3038
- }
3039
- var FileSyncStateStore = class {
3040
- constructor(directory, projectUri) {
3041
- this.projectUri = projectUri;
3042
- const overleafDirectory = resolve4(directory, ".overleaf");
3043
- this.legacyPath = join2(overleafDirectory, "sync-state.json");
3044
- this.syncDirectory = join2(overleafDirectory, "sync");
3045
- this.formatPath = join2(this.syncDirectory, "format.json");
3046
- this.cursorPath = join2(this.syncDirectory, "cursor.json");
3047
- this.pathsDirectory = join2(this.syncDirectory, "paths");
3048
- this.basesDirectory = join2(this.syncDirectory, "bases");
3049
- }
3050
- projectUri;
3051
- legacyPath;
3052
- syncDirectory;
3053
- formatPath;
3054
- cursorPath;
3055
- pathsDirectory;
3056
- basesDirectory;
3057
- persistedRecords = /* @__PURE__ */ new Map();
3058
- persistedCursor;
3059
- invalidRecordPaths = /* @__PURE__ */ new Set();
3060
- initialized = false;
3061
- async load() {
3062
- this.invalidRecordPaths.clear();
3063
- try {
3064
- const format = await readJson(this.formatPath);
3065
- const cursor = await readJson(this.cursorPath);
3066
- if (!parseFormat(format, this.projectUri) || !parseCursor(cursor)) {
3067
- return void 0;
3068
- }
3069
- const state = {
3070
- schemaVersion: SYNC_STATE_SCHEMA_VERSION,
3071
- projectUri: this.projectUri,
3072
- remoteVersion: cursor.remoteVersion,
3073
- initialized: cursor.initialized,
3074
- files: {},
3075
- textBases: {},
3076
- conflicts: {}
3077
- };
3078
- const records = /* @__PURE__ */ new Map();
3079
- const referencedBases = /* @__PURE__ */ new Set();
3080
- let stateIncomplete = false;
3081
- for (const recordPath of await listFiles(this.pathsDirectory)) {
3082
- const record = await this.loadRecord(recordPath);
3083
- if (!record) {
3084
- if (recordPath.endsWith(".json")) {
3085
- this.invalidRecordPaths.add(recordPath);
3086
- stateIncomplete = true;
3087
- } else {
3088
- await rm2(recordPath, { force: true });
3089
- }
3090
- continue;
3091
- }
3092
- if (record.hash !== void 0) {
3093
- state.files[record.path] = record.hash;
3094
- }
3095
- if (record.baseHash !== void 0) {
3096
- let textBase;
3097
- try {
3098
- textBase = await readFile4(this.basePath(record.baseHash), "utf8");
3099
- } catch (error2) {
3100
- if (error2.code === "ENOENT") {
3101
- this.invalidRecordPaths.add(recordPath);
3102
- stateIncomplete = true;
3103
- delete state.files[record.path];
3104
- continue;
3105
- }
3106
- throw error2;
3107
- }
3108
- if (sha256(encoder.encode(textBase)) !== record.baseHash) {
3109
- this.invalidRecordPaths.add(recordPath);
3110
- stateIncomplete = true;
3111
- delete state.files[record.path];
3112
- continue;
3113
- }
3114
- state.textBases[record.path] = textBase;
3115
- referencedBases.add(record.baseHash);
3116
- }
3117
- if (record.conflict !== void 0) {
3118
- state.conflicts[record.path] = record.conflict;
3119
- }
3120
- records.set(record.path, json(record));
3121
- }
3122
- if (stateIncomplete) {
3123
- state.initialized = false;
3124
- }
3125
- await this.removeOrphanBases(referencedBases);
3126
- this.persistedRecords = records;
3127
- this.persistedCursor = json(cursor);
3128
- this.initialized = true;
3129
- return state;
3130
- } catch (error2) {
3131
- if (error2.code === "ENOENT" || error2 instanceof SyntaxError) {
3132
- return void 0;
3133
- }
3134
- throw error2;
3135
- }
3136
- }
3137
- async save(state) {
3138
- if (state.projectUri !== this.projectUri || state.schemaVersion !== SYNC_STATE_SCHEMA_VERSION) {
3139
- throw new Error("Cannot persist synchronization state for a different project or schema.");
3140
- }
3141
- if (!this.initialized) {
3142
- await this.initializeStorage();
3143
- }
3144
- const paths = /* @__PURE__ */ new Set([
3145
- ...this.persistedRecords.keys(),
3146
- ...Object.keys(state.files),
3147
- ...Object.keys(state.conflicts)
3148
- ]);
3149
- const obsoleteBases = /* @__PURE__ */ new Set();
3150
- for (const path of [...paths].sort()) {
3151
- const record = createRecord(state, path);
3152
- const serialized = record && json(record);
3153
- const previous = this.persistedRecords.get(path);
3154
- if (serialized === previous) {
3155
- continue;
3156
- }
3157
- if (previous) {
3158
- const previousRecord = JSON.parse(previous);
3159
- if (previousRecord.baseHash) {
3160
- obsoleteBases.add(previousRecord.baseHash);
3161
- }
3162
- }
3163
- if (!record || !serialized) {
3164
- await rm2(this.recordPath(path), { force: true });
3165
- this.persistedRecords.delete(path);
3166
- continue;
3167
- }
3168
- if (record.baseHash) {
3169
- const textBase = state.textBases[path];
3170
- if (textBase === void 0 || sha256(encoder.encode(textBase)) !== record.baseHash) {
3171
- throw new Error(`Text checkpoint does not match its content hash: ${path}`);
3172
- }
3173
- await this.writeBase(record.baseHash, textBase);
3174
- }
3175
- const destination = this.recordPath(path);
3176
- await atomicWrite(destination, serialized);
3177
- this.invalidRecordPaths.delete(destination);
3178
- this.persistedRecords.set(path, serialized);
3179
- }
3180
- const cursor = json({ remoteVersion: state.remoteVersion, initialized: state.initialized });
3181
- if (cursor !== this.persistedCursor) {
3182
- await atomicWrite(this.cursorPath, cursor);
3183
- this.persistedCursor = cursor;
3184
- }
3185
- if (state.initialized) {
3186
- await Promise.all([...this.invalidRecordPaths].map((path) => rm2(path, { force: true })));
3187
- this.invalidRecordPaths.clear();
3188
- }
3189
- await rm2(this.legacyPath, { force: true });
3190
- await this.removeObsoleteBases(state, obsoleteBases);
3191
- }
3192
- async initializeStorage() {
3193
- await rm2(this.syncDirectory, { recursive: true, force: true });
3194
- await mkdir3(this.pathsDirectory, { recursive: true });
3195
- await mkdir3(this.basesDirectory, { recursive: true });
3196
- await atomicWrite(this.formatPath, json({
3197
- schemaVersion: SYNC_STATE_SCHEMA_VERSION,
3198
- projectUri: this.projectUri
3199
- }));
3200
- this.persistedRecords.clear();
3201
- this.persistedCursor = void 0;
3202
- this.invalidRecordPaths.clear();
3203
- this.initialized = true;
3204
- }
3205
- recordPath(path) {
3206
- const key = pathKey(path);
3207
- return join2(this.pathsDirectory, key.slice(0, 2), `${key}.json`);
3208
- }
3209
- async loadRecord(recordPath) {
3210
- if (!recordPath.endsWith(".json")) {
3211
- return void 0;
3212
- }
3213
- try {
3214
- const record = await readJson(recordPath);
3215
- if (!parsePathRecord(record) || recordPath !== this.recordPath(record.path)) {
3216
- return void 0;
3217
- }
3218
- return record;
3219
- } catch (error2) {
3220
- if (error2.code === "ENOENT" || error2 instanceof SyntaxError) {
3221
- return void 0;
3222
- }
3223
- throw error2;
3224
- }
3225
- }
3226
- basePath(hash) {
3227
- return join2(this.basesDirectory, hash.slice(0, 2), `${hash}.txt`);
3228
- }
3229
- async writeBase(hash, content) {
3230
- const path = this.basePath(hash);
3231
- try {
3232
- if (sha256(encoder.encode(await readFile4(path, "utf8"))) === hash) {
3233
- return;
3234
- }
3235
- } catch (error2) {
3236
- if (error2.code !== "ENOENT") {
3237
- throw error2;
3238
- }
3239
- }
3240
- await atomicWrite(path, content);
3241
- }
3242
- async removeObsoleteBases(state, candidates) {
3243
- if (candidates.size === 0) {
3244
- return;
3245
- }
3246
- const referenced = /* @__PURE__ */ new Set();
3247
- for (const [path, textBase] of Object.entries(state.textBases)) {
3248
- if (textBase !== void 0 && state.files[path] !== void 0) {
3249
- referenced.add(state.files[path]);
3250
- }
3251
- }
3252
- for (const hash of candidates) {
3253
- if (!referenced.has(hash)) {
3254
- await rm2(this.basePath(hash), { force: true });
3255
- }
3256
- }
3257
- }
3258
- async removeOrphanBases(referencedHashes) {
3259
- const referencedPaths = new Set([...referencedHashes].map((hash) => this.basePath(hash)));
3260
- for (const path of await listFiles(this.basesDirectory)) {
3261
- if (!referencedPaths.has(path)) {
3262
- await rm2(path, { force: true });
3263
- }
3264
- }
3265
- }
3266
- };
3267
- async function startServe(directory = process.cwd(), options = {}) {
3268
- const root = resolve5(directory);
3269
- const settings = await readProjectSettings(root);
3270
- const session = await getServerSession(settings.serverName);
3271
- if (!session) {
3272
- throw new Error(`No login is stored for ${settings.serverName}. Run: leafrelay login ${settings.serverName}`);
3273
- }
3274
- const cookie = options.cookie || process.env.LEAFRELAY_COOKIE || session.identity.cookies;
3275
- const remote = options.remote ?? new RemoteProject(session.url || settings.serverUrl, settings.projectId, cookie);
3276
- await remote.connect();
3277
- const configuredPatterns = settings.localReplica.settings["ignore-patterns"];
3278
- const ignore = createIgnoreMatcher(configuredPatterns);
3279
- const local = new NodeReplicaFileSystem(root, ignore);
3280
- const engine = new SyncEngine({
3281
- projectUri: settings.uri,
3282
- local,
3283
- remote,
3284
- stateStore: new FileSyncStateStore(root, settings.uri),
3285
- ignore,
3286
- onSyncError: options.onSyncError,
3287
- log: options.log ?? log,
3288
- onConflict: options.onConflict ?? ((path, reason) => {
3289
- (options.log ?? log)(`Conflict paused for ${path}: ${reason}`);
3290
- })
3291
- });
3292
- try {
3293
- await engine.start();
3294
- } catch (error2) {
3295
- remote.disconnect();
3296
- throw error2;
3297
- }
3298
- (options.log ?? log)(`LeafRelay is serving ${root} (${settings.projectName}).`);
3299
- return {
3300
- root,
3301
- serverName: settings.serverName,
3302
- projectId: settings.projectId,
3303
- projectName: settings.projectName,
3304
- syncError: () => engine.syncError,
3305
- conflicts: () => engine.getConflicts(),
3306
- resolveConflict: (path, winner) => engine.resolveConflict(path, winner),
3307
- retry: (path) => engine.retry(path),
3308
- stop: async () => {
3309
- await engine.stop();
3310
- remote.disconnect();
3311
- }
3312
- };
3313
- }
3314
-
3315
- export {
3316
- setLogSink,
3317
- HttpRequestScheduler,
3318
- ExtendedBaseAPI,
3319
- SocketIOAPI,
3320
- configPath,
3321
- normalizeServerUrl,
3322
- serverKey,
3323
- readConfig,
3324
- saveServerSession,
3325
- saveKnownServer,
3326
- removeKnownServer,
3327
- removeServerSession,
3328
- getServerSession,
3329
- readProjectSettings,
3330
- RemoteProject,
3331
- startServe
3332
- };
3333
- //# sourceMappingURL=chunk-3TTRDNHP.js.map