leafrelay 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/dist/{chunk-MNOPLM7M.js → chunk-Q3R5N4YE.js} +201 -133
- package/dist/chunk-Q3R5N4YE.js.map +1 -0
- package/dist/chunk-UO7Z3BQN.js +5157 -0
- package/dist/chunk-UO7Z3BQN.js.map +1 -0
- package/dist/cli.js +21 -36
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +959 -0
- package/dist/daemon.js.map +1 -0
- package/dist/index.d.ts +11 -722
- package/dist/index.js +3 -69
- package/package.json +6 -3
- package/dist/chunk-MNOPLM7M.js.map +0 -1
- package/dist/cli.d.ts +0 -1
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ cd /path/to/local-replica
|
|
|
9
9
|
leafrelay serve
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`leafrelay serve` reads `.overleaf/settings.json` from the local project
|
|
12
|
+
`leafrelay serve` reads `.overleaf/settings.json` from the local project and connects to the machine-wide LeafRelay daemon. VS Code windows and other CLI processes use that same daemon, request scheduler, project connection, and local watcher.
|
|
13
|
+
|
|
14
|
+
Login sessions are stored per server in `~/.leafrelay/config.json`. Use `leafrelay daemon status|stop|restart` to control the daemon. `LEAFRELAY_HOME` relocates all LeafRelay user state, `LEAFRELAY_CONFIG` selects another login file, and `LEAFRELAY_COOKIE` overrides the saved cookie for the current process.
|
|
13
15
|
|
|
14
16
|
For the VS Code extension, architecture, and project documentation, see the [LeafRelay repository](https://github.com/yunhaoli24/leafrelay).
|
package/README.zh-CN.md
CHANGED
|
@@ -9,6 +9,8 @@ cd /path/to/local-replica
|
|
|
9
9
|
leafrelay serve
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`leafrelay serve` 会读取项目中的 `.overleaf/settings.json
|
|
12
|
+
`leafrelay serve` 会读取项目中的 `.overleaf/settings.json`,然后连接这台电脑上唯一的 LeafRelay daemon。VS Code 窗口和其他 CLI 进程共用同一个后台、请求队列、项目连接和本地 watcher。
|
|
13
|
+
|
|
14
|
+
登录会话按服务器保存到 `~/.leafrelay/config.json`。使用 `leafrelay daemon status|stop|restart` 控制后台进程。`LEAFRELAY_HOME` 可以移动全部 LeafRelay 用户状态,`LEAFRELAY_CONFIG` 只指定其他登录文件,`LEAFRELAY_COOKIE` 则仅覆盖当前进程使用的 cookie。
|
|
13
15
|
|
|
14
16
|
VS Code 扩展、架构和完整文档见 [LeafRelay 仓库](https://github.com/yunhaoli24/leafrelay)。
|
|
@@ -220,7 +220,7 @@ var OverleafRealtimeSocket = class {
|
|
|
220
220
|
handshakeUrl.searchParams.set(name, value);
|
|
221
221
|
}
|
|
222
222
|
handshakeUrl.searchParams.set("t", String(Date.now()));
|
|
223
|
-
const response = await fetch(handshakeUrl, {
|
|
223
|
+
const response = await (this.options.fetch ?? fetch)(handshakeUrl, {
|
|
224
224
|
headers: { origin: this.origin, cookie: this.cookie },
|
|
225
225
|
redirect: "manual"
|
|
226
226
|
});
|
|
@@ -373,11 +373,7 @@ var OverleafRealtimeSocket = class {
|
|
|
373
373
|
}, delay);
|
|
374
374
|
}
|
|
375
375
|
};
|
|
376
|
-
var overleafSocketProtocol = { encodePacket, decodePacket, decodePayload };
|
|
377
376
|
var sink;
|
|
378
|
-
function setLogSink(nextSink) {
|
|
379
|
-
sink = nextSink;
|
|
380
|
-
}
|
|
381
377
|
function write(level, args) {
|
|
382
378
|
if (sink !== void 0) {
|
|
383
379
|
sink(level, args);
|
|
@@ -389,57 +385,75 @@ function write(level, args) {
|
|
|
389
385
|
function log(...args) {
|
|
390
386
|
write("info", args);
|
|
391
387
|
}
|
|
392
|
-
function warn(...args) {
|
|
393
|
-
write("warn", args);
|
|
394
|
-
}
|
|
395
388
|
function error(...args) {
|
|
396
389
|
write("error", args);
|
|
397
390
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
var GLOBAL_MIN_REQUEST_INTERVAL_MS = 300;
|
|
402
|
-
function retryAfterHeaderMs(response) {
|
|
403
|
-
const retryAfter = response.headers?.get?.("retry-after");
|
|
404
|
-
if (retryAfter !== void 0 && retryAfter !== null) {
|
|
391
|
+
function retryAfterMilliseconds(response, now) {
|
|
392
|
+
const retryAfter = response.headers.get("retry-after");
|
|
393
|
+
if (retryAfter) {
|
|
405
394
|
const seconds = Number(retryAfter);
|
|
406
395
|
if (Number.isFinite(seconds)) {
|
|
407
396
|
return Math.max(1e3, seconds * 1e3);
|
|
408
397
|
}
|
|
409
398
|
const retryAt = Date.parse(retryAfter);
|
|
410
399
|
if (Number.isFinite(retryAt)) {
|
|
411
|
-
return Math.max(1e3, retryAt -
|
|
400
|
+
return Math.max(1e3, retryAt - now);
|
|
412
401
|
}
|
|
413
402
|
}
|
|
414
403
|
return 5e3;
|
|
415
404
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const delayMs = Math.max(
|
|
425
|
-
0,
|
|
426
|
-
globalNextRequestAt - Date.now(),
|
|
427
|
-
globalRateLimitedUntil - Date.now()
|
|
428
|
-
);
|
|
429
|
-
if (delayMs > 0) {
|
|
430
|
-
await new Promise((resolve6) => setTimeout(resolve6, delayMs));
|
|
431
|
-
}
|
|
432
|
-
globalNextRequestAt = Date.now() + GLOBAL_MIN_REQUEST_INTERVAL_MS;
|
|
433
|
-
const response = await fetch(url, init);
|
|
434
|
-
if (response.status === 429) {
|
|
435
|
-
globalRateLimitedUntil = Math.max(globalRateLimitedUntil, Date.now() + retryAfterHeaderMs(response));
|
|
436
|
-
log(`Global HTTP request queue entered rate-limit cooldown until ${new Date(globalRateLimitedUntil).toISOString()}.`);
|
|
437
|
-
}
|
|
438
|
-
return response;
|
|
439
|
-
} finally {
|
|
440
|
-
release();
|
|
405
|
+
var HttpRequestScheduler = class {
|
|
406
|
+
constructor(options = {}) {
|
|
407
|
+
this.options = options;
|
|
408
|
+
this.minimumIntervalMs = options.minimumIntervalMs ?? 300;
|
|
409
|
+
this.historyIntervalMs = options.historyIntervalMs ?? 3e3;
|
|
410
|
+
this.fetchImplementation = options.fetch ?? fetch;
|
|
411
|
+
this.now = options.now ?? Date.now;
|
|
412
|
+
this.wait = options.wait ?? ((milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds)));
|
|
441
413
|
}
|
|
442
|
-
|
|
414
|
+
options;
|
|
415
|
+
chain = Promise.resolve();
|
|
416
|
+
nextRequestAt = 0;
|
|
417
|
+
historyNextRequestAt = 0;
|
|
418
|
+
rateLimitedUntil = 0;
|
|
419
|
+
minimumIntervalMs;
|
|
420
|
+
historyIntervalMs;
|
|
421
|
+
fetchImplementation;
|
|
422
|
+
now;
|
|
423
|
+
wait;
|
|
424
|
+
fetch(input, init) {
|
|
425
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
426
|
+
const history = url.includes("/updates?") || url.includes("/filetree/diff?") || url.includes("/diff?");
|
|
427
|
+
return this.enqueue(async () => {
|
|
428
|
+
const now = this.now();
|
|
429
|
+
const readyAt = Math.max(this.nextRequestAt, this.rateLimitedUntil, history ? this.historyNextRequestAt : 0);
|
|
430
|
+
if (readyAt > now) {
|
|
431
|
+
await this.wait(readyAt - now);
|
|
432
|
+
}
|
|
433
|
+
const requestAt = this.now();
|
|
434
|
+
this.nextRequestAt = requestAt + this.minimumIntervalMs;
|
|
435
|
+
if (history) {
|
|
436
|
+
this.historyNextRequestAt = requestAt + this.historyIntervalMs;
|
|
437
|
+
}
|
|
438
|
+
const response = await this.fetchImplementation(input, init);
|
|
439
|
+
if (response.status === 429) {
|
|
440
|
+
this.rateLimitedUntil = Math.max(
|
|
441
|
+
this.rateLimitedUntil,
|
|
442
|
+
this.now() + retryAfterMilliseconds(response, this.now())
|
|
443
|
+
);
|
|
444
|
+
this.options.onRateLimit?.(this.rateLimitedUntil);
|
|
445
|
+
log(`HTTP request scheduler entered rate-limit cooldown until ${new Date(this.rateLimitedUntil).toISOString()}.`);
|
|
446
|
+
}
|
|
447
|
+
return response;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
enqueue(operation) {
|
|
451
|
+
const result = this.chain.then(operation);
|
|
452
|
+
this.chain = result.then(() => void 0, () => void 0);
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
var sharedHttpRequestScheduler = new HttpRequestScheduler();
|
|
443
457
|
function getSetCookie(res) {
|
|
444
458
|
if (typeof res.headers?.getSetCookie === "function") {
|
|
445
459
|
return res.headers.getSetCookie();
|
|
@@ -451,31 +465,13 @@ function getSetCookie(res) {
|
|
|
451
465
|
return [];
|
|
452
466
|
}
|
|
453
467
|
var BaseAPI = class {
|
|
468
|
+
constructor(url, scheduler = sharedHttpRequestScheduler) {
|
|
469
|
+
this.scheduler = scheduler;
|
|
470
|
+
this.url = url;
|
|
471
|
+
}
|
|
472
|
+
scheduler;
|
|
454
473
|
url;
|
|
455
474
|
identity;
|
|
456
|
-
historyRequestChain = Promise.resolve();
|
|
457
|
-
lastHistoryRequestAt = 0;
|
|
458
|
-
isHistoryRoute(route) {
|
|
459
|
-
return route.includes("/updates?") || route.includes("/filetree/diff?") || route.includes("/diff?");
|
|
460
|
-
}
|
|
461
|
-
async waitForHistoryRequest() {
|
|
462
|
-
const previous = this.historyRequestChain;
|
|
463
|
-
let release;
|
|
464
|
-
this.historyRequestChain = new Promise((resolve6) => {
|
|
465
|
-
release = resolve6;
|
|
466
|
-
});
|
|
467
|
-
await previous;
|
|
468
|
-
try {
|
|
469
|
-
const minimumIntervalMs = 3e3;
|
|
470
|
-
const delayMs = Math.max(0, minimumIntervalMs - (Date.now() - this.lastHistoryRequestAt));
|
|
471
|
-
if (delayMs > 0) {
|
|
472
|
-
await new Promise((resolve6) => setTimeout(resolve6, delayMs));
|
|
473
|
-
}
|
|
474
|
-
this.lastHistoryRequestAt = Date.now();
|
|
475
|
-
} finally {
|
|
476
|
-
release();
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
475
|
retryAfterMs(response, attempt) {
|
|
480
476
|
const retryAfter = response.headers?.get?.("retry-after");
|
|
481
477
|
if (retryAfter !== void 0 && retryAfter !== null) {
|
|
@@ -490,11 +486,8 @@ var BaseAPI = class {
|
|
|
490
486
|
}
|
|
491
487
|
return 5e3 * Math.pow(2, attempt);
|
|
492
488
|
}
|
|
493
|
-
constructor(url) {
|
|
494
|
-
this.url = url;
|
|
495
|
-
}
|
|
496
489
|
async getCsrfToken() {
|
|
497
|
-
const res = await
|
|
490
|
+
const res = await this.scheduler.fetch(this.url + "login", {
|
|
498
491
|
method: "GET",
|
|
499
492
|
redirect: "manual"
|
|
500
493
|
});
|
|
@@ -509,7 +502,7 @@ var BaseAPI = class {
|
|
|
509
502
|
}
|
|
510
503
|
}
|
|
511
504
|
async getUserId(cookies) {
|
|
512
|
-
const res = await
|
|
505
|
+
const res = await this.scheduler.fetch(this.url + "project", {
|
|
513
506
|
method: "GET",
|
|
514
507
|
redirect: "manual",
|
|
515
508
|
headers: {
|
|
@@ -539,12 +532,13 @@ var BaseAPI = class {
|
|
|
539
532
|
reconnect: true,
|
|
540
533
|
reconnectionDelay: 1e3,
|
|
541
534
|
reconnectionLimit: 16e3,
|
|
542
|
-
maxReconnectionAttempts: 10
|
|
535
|
+
maxReconnectionAttempts: 10,
|
|
536
|
+
fetch: (input, init) => this.scheduler.fetch(input, init)
|
|
543
537
|
});
|
|
544
538
|
}
|
|
545
539
|
async passportLogin(email, password) {
|
|
546
540
|
const identity = await this.getCsrfToken();
|
|
547
|
-
const res = await
|
|
541
|
+
const res = await this.scheduler.fetch(this.url + "login", {
|
|
548
542
|
method: "POST",
|
|
549
543
|
redirect: "manual",
|
|
550
544
|
headers: {
|
|
@@ -603,7 +597,7 @@ var BaseAPI = class {
|
|
|
603
597
|
}
|
|
604
598
|
}
|
|
605
599
|
async updateCookies(identity) {
|
|
606
|
-
const res = await
|
|
600
|
+
const res = await this.scheduler.fetch(this.url + "socket.io/socket.io.js", {
|
|
607
601
|
method: "GET",
|
|
608
602
|
redirect: "manual",
|
|
609
603
|
headers: {
|
|
@@ -645,13 +639,10 @@ var BaseAPI = class {
|
|
|
645
639
|
let lastError = {};
|
|
646
640
|
for (let attempt = 0; attempt <= MAX_HTTP_RETRIES; attempt++) {
|
|
647
641
|
try {
|
|
648
|
-
if (this.isHistoryRoute(route)) {
|
|
649
|
-
await this.waitForHistoryRequest();
|
|
650
|
-
}
|
|
651
642
|
let res = void 0;
|
|
652
643
|
switch (type) {
|
|
653
644
|
case "GET":
|
|
654
|
-
res = await
|
|
645
|
+
res = await this.scheduler.fetch(this.url + route, {
|
|
655
646
|
method: "GET",
|
|
656
647
|
redirect: "manual",
|
|
657
648
|
headers: {
|
|
@@ -667,7 +658,7 @@ var BaseAPI = class {
|
|
|
667
658
|
_csrf: this.identity.csrfToken,
|
|
668
659
|
...body
|
|
669
660
|
});
|
|
670
|
-
res = await
|
|
661
|
+
res = await this.scheduler.fetch(this.url + route, {
|
|
671
662
|
method: "POST",
|
|
672
663
|
redirect: "manual",
|
|
673
664
|
headers: {
|
|
@@ -682,7 +673,7 @@ var BaseAPI = class {
|
|
|
682
673
|
case "PUT":
|
|
683
674
|
break;
|
|
684
675
|
case "DELETE":
|
|
685
|
-
res = await
|
|
676
|
+
res = await this.scheduler.fetch(this.url + route, {
|
|
686
677
|
method: "DELETE",
|
|
687
678
|
redirect: "manual",
|
|
688
679
|
headers: {
|
|
@@ -749,7 +740,7 @@ var BaseAPI = class {
|
|
|
749
740
|
}
|
|
750
741
|
let content = [];
|
|
751
742
|
while (true) {
|
|
752
|
-
const res = await
|
|
743
|
+
const res = await this.scheduler.fetch(this.url + route, {
|
|
753
744
|
method: "GET",
|
|
754
745
|
redirect: "manual",
|
|
755
746
|
headers: {
|
|
@@ -1030,7 +1021,7 @@ var BaseAPI = class {
|
|
|
1030
1021
|
}
|
|
1031
1022
|
let content = [];
|
|
1032
1023
|
while (true) {
|
|
1033
|
-
const res = await
|
|
1024
|
+
const res = await this.scheduler.fetch(absoluteUrl, {
|
|
1034
1025
|
method: "GET",
|
|
1035
1026
|
redirect: "manual",
|
|
1036
1027
|
headers
|
|
@@ -1517,14 +1508,8 @@ var SocketIOAPI = class _SocketIOAPI {
|
|
|
1517
1508
|
});
|
|
1518
1509
|
}
|
|
1519
1510
|
};
|
|
1520
|
-
var FolderKeys = {
|
|
1521
|
-
folder: "folders",
|
|
1522
|
-
doc: "docs",
|
|
1523
|
-
file: "fileRefs",
|
|
1524
|
-
outputs: "outputs"
|
|
1525
|
-
};
|
|
1526
1511
|
function configPath(environment = process.env) {
|
|
1527
|
-
return environment.LEAFRELAY_CONFIG ? resolve(environment.LEAFRELAY_CONFIG) : join(homedir(), ".leafrelay", "config.json");
|
|
1512
|
+
return environment.LEAFRELAY_CONFIG ? resolve(environment.LEAFRELAY_CONFIG) : join(environment.LEAFRELAY_HOME ? resolve(environment.LEAFRELAY_HOME) : join(homedir(), ".leafrelay"), "config.json");
|
|
1528
1513
|
}
|
|
1529
1514
|
function normalizeServerUrl(value) {
|
|
1530
1515
|
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
@@ -1545,7 +1530,7 @@ async function readConfig(path = configPath()) {
|
|
|
1545
1530
|
return parsed;
|
|
1546
1531
|
} catch (error2) {
|
|
1547
1532
|
if (error2.code === "ENOENT") {
|
|
1548
|
-
return { version: 1, servers: {} };
|
|
1533
|
+
return { version: 1, servers: {}, knownServers: {} };
|
|
1549
1534
|
}
|
|
1550
1535
|
throw error2;
|
|
1551
1536
|
}
|
|
@@ -1558,7 +1543,29 @@ async function writeConfig(config, path = configPath()) {
|
|
|
1558
1543
|
}
|
|
1559
1544
|
async function saveServerSession(session, path = configPath()) {
|
|
1560
1545
|
const config = await readConfig(path);
|
|
1561
|
-
|
|
1546
|
+
const key = serverKey(session.url);
|
|
1547
|
+
config.servers[key] = session;
|
|
1548
|
+
config.knownServers ??= {};
|
|
1549
|
+
config.knownServers[key] ??= { name: key, url: session.url };
|
|
1550
|
+
await writeConfig(config, path);
|
|
1551
|
+
}
|
|
1552
|
+
async function saveKnownServer(name, url, path = configPath()) {
|
|
1553
|
+
const config = await readConfig(path);
|
|
1554
|
+
const key = serverKey(url);
|
|
1555
|
+
config.knownServers ??= {};
|
|
1556
|
+
config.knownServers[key] = { name, url: normalizeServerUrl(url) };
|
|
1557
|
+
await writeConfig(config, path);
|
|
1558
|
+
}
|
|
1559
|
+
async function removeKnownServer(server, path = configPath()) {
|
|
1560
|
+
const config = await readConfig(path);
|
|
1561
|
+
const key = serverKey(server);
|
|
1562
|
+
delete config.servers[key];
|
|
1563
|
+
delete config.knownServers?.[key];
|
|
1564
|
+
await writeConfig(config, path);
|
|
1565
|
+
}
|
|
1566
|
+
async function removeServerSession(server, path = configPath()) {
|
|
1567
|
+
const config = await readConfig(path);
|
|
1568
|
+
delete config.servers[serverKey(server)];
|
|
1562
1569
|
await writeConfig(config, path);
|
|
1563
1570
|
}
|
|
1564
1571
|
async function getServerSession(server, environment = process.env, path = configPath(environment)) {
|
|
@@ -1744,11 +1751,12 @@ function textContent(content) {
|
|
|
1744
1751
|
}
|
|
1745
1752
|
}
|
|
1746
1753
|
var RemoteProject = class {
|
|
1747
|
-
constructor(url, projectId, cookies) {
|
|
1754
|
+
constructor(url, projectId, cookies, transport) {
|
|
1748
1755
|
this.url = url;
|
|
1749
1756
|
this.projectId = projectId;
|
|
1750
1757
|
this.cookies = cookies;
|
|
1751
|
-
this.
|
|
1758
|
+
this.externalTransport = transport;
|
|
1759
|
+
this.api = transport?.api ?? new BaseAPI(url);
|
|
1752
1760
|
}
|
|
1753
1761
|
url;
|
|
1754
1762
|
projectId;
|
|
@@ -1760,19 +1768,27 @@ var RemoteProject = class {
|
|
|
1760
1768
|
entries = /* @__PURE__ */ new Map();
|
|
1761
1769
|
idToPath = /* @__PURE__ */ new Map();
|
|
1762
1770
|
changeHandler;
|
|
1771
|
+
externalTransport;
|
|
1763
1772
|
async connect() {
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1773
|
+
if (this.externalTransport) {
|
|
1774
|
+
this.identity = this.externalTransport.identity;
|
|
1775
|
+
this.socket = this.externalTransport.socket;
|
|
1776
|
+
} else {
|
|
1777
|
+
const login = await this.api.cookiesLogin(this.cookies);
|
|
1778
|
+
if (login.type !== "success" || !login.identity) {
|
|
1779
|
+
throw new Error(login.message || `Authentication to ${this.url} failed.`);
|
|
1780
|
+
}
|
|
1781
|
+
this.identity = login.identity;
|
|
1782
|
+
this.socket = new SocketIOAPI(this.url, this.api, this.identity, this.projectId);
|
|
1767
1783
|
}
|
|
1768
|
-
this.identity = login.identity;
|
|
1769
|
-
this.socket = new SocketIOAPI(this.url, this.api, this.identity, this.projectId);
|
|
1770
1784
|
this.project = await this.socket.joinProject(this.projectId);
|
|
1771
1785
|
this.reindex();
|
|
1772
1786
|
this.registerEvents();
|
|
1773
1787
|
}
|
|
1774
1788
|
disconnect() {
|
|
1775
|
-
this.
|
|
1789
|
+
if (!this.externalTransport) {
|
|
1790
|
+
this.socket?.disconnect();
|
|
1791
|
+
}
|
|
1776
1792
|
}
|
|
1777
1793
|
onChange(handler) {
|
|
1778
1794
|
this.changeHandler = handler;
|
|
@@ -2205,6 +2221,49 @@ var SyncEngine = class {
|
|
|
2205
2221
|
await this.stopLocalWatch?.();
|
|
2206
2222
|
await this.queue;
|
|
2207
2223
|
}
|
|
2224
|
+
getConflicts() {
|
|
2225
|
+
return [...this.conflicts].sort();
|
|
2226
|
+
}
|
|
2227
|
+
async resolveConflict(path, winner) {
|
|
2228
|
+
const normalized = normalizePath2(path);
|
|
2229
|
+
if (!this.conflicts.has(normalized)) {
|
|
2230
|
+
throw new Error(`No unresolved conflict exists for ${normalized}.`);
|
|
2231
|
+
}
|
|
2232
|
+
await this.enqueue(async () => {
|
|
2233
|
+
const remoteEntry = this.options.remote.entry(normalized);
|
|
2234
|
+
if (winner === "local") {
|
|
2235
|
+
const localContent = await this.options.local.read(normalized);
|
|
2236
|
+
if (localContent) {
|
|
2237
|
+
await this.options.remote.write(normalized, localContent);
|
|
2238
|
+
const synchronized = await this.options.remote.read(normalized);
|
|
2239
|
+
if (sha256(synchronized) !== sha256(localContent)) {
|
|
2240
|
+
await this.options.local.write(normalized, synchronized);
|
|
2241
|
+
}
|
|
2242
|
+
updateSyncCheckpoint(this.state, normalized, synchronized);
|
|
2243
|
+
} else {
|
|
2244
|
+
if (remoteEntry) {
|
|
2245
|
+
await this.options.remote.remove(normalized);
|
|
2246
|
+
}
|
|
2247
|
+
removeSyncCheckpoint(this.state, normalized);
|
|
2248
|
+
}
|
|
2249
|
+
} else if (isRemoteFile(remoteEntry)) {
|
|
2250
|
+
const remoteContent = await this.options.remote.read(normalized);
|
|
2251
|
+
await this.options.local.write(normalized, remoteContent);
|
|
2252
|
+
updateSyncCheckpoint(this.state, normalized, remoteContent);
|
|
2253
|
+
} else {
|
|
2254
|
+
await this.options.local.remove(normalized);
|
|
2255
|
+
removeSyncCheckpoint(this.state, normalized);
|
|
2256
|
+
}
|
|
2257
|
+
await this.options.stateStore.save(this.state);
|
|
2258
|
+
this.conflicts.delete(normalized);
|
|
2259
|
+
this.options.log(`Resolved ${normalized} using ${winner === "local" ? "local" : "Overleaf"} content.`);
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
async retry(path) {
|
|
2263
|
+
const normalized = normalizePath2(path);
|
|
2264
|
+
this.conflicts.delete(normalized);
|
|
2265
|
+
await this.enqueue(() => this.reconcilePath(normalized, "local"));
|
|
2266
|
+
}
|
|
2208
2267
|
async startupReconcile(currentVersion, localFiles, remoteFiles) {
|
|
2209
2268
|
const state = this.state;
|
|
2210
2269
|
if (state.remoteVersion > currentVersion) {
|
|
@@ -2256,11 +2315,14 @@ var SyncEngine = class {
|
|
|
2256
2315
|
this.options.log(`Failed to synchronize ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
2257
2316
|
}
|
|
2258
2317
|
}
|
|
2259
|
-
if (failed
|
|
2260
|
-
throw new Error(
|
|
2318
|
+
if (failed) {
|
|
2319
|
+
throw new Error("Synchronization paused for failed paths; successful paths were checkpointed.");
|
|
2261
2320
|
}
|
|
2262
2321
|
state.remoteVersion = currentVersion;
|
|
2263
2322
|
await this.options.stateStore.save(state);
|
|
2323
|
+
if (this.conflicts.size !== 0) {
|
|
2324
|
+
this.options.log(`Synchronization remains active; ${this.conflicts.size} conflicting path(s) are paused.`);
|
|
2325
|
+
}
|
|
2264
2326
|
}
|
|
2265
2327
|
schedule(path, source) {
|
|
2266
2328
|
const normalized = normalizePath2(path);
|
|
@@ -2278,6 +2340,11 @@ var SyncEngine = class {
|
|
|
2278
2340
|
});
|
|
2279
2341
|
}, 250));
|
|
2280
2342
|
}
|
|
2343
|
+
enqueue(operation) {
|
|
2344
|
+
const result = this.queue.then(operation);
|
|
2345
|
+
this.queue = result.then(() => void 0, () => void 0);
|
|
2346
|
+
return result;
|
|
2347
|
+
}
|
|
2281
2348
|
async reconcilePath(path, source) {
|
|
2282
2349
|
const normalized = normalizePath2(path);
|
|
2283
2350
|
if (this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
@@ -2312,7 +2379,7 @@ var SyncEngine = class {
|
|
|
2312
2379
|
await this.options.remote.write(normalized, merged.content);
|
|
2313
2380
|
updateSyncCheckpoint(this.state, normalized, merged.content);
|
|
2314
2381
|
await this.options.stateStore.save(this.state);
|
|
2315
|
-
this.
|
|
2382
|
+
this.logChange("merge", "update", normalized);
|
|
2316
2383
|
return;
|
|
2317
2384
|
}
|
|
2318
2385
|
}
|
|
@@ -2336,6 +2403,11 @@ var SyncEngine = class {
|
|
|
2336
2403
|
updateSyncCheckpoint(this.state, normalized, synchronized);
|
|
2337
2404
|
}
|
|
2338
2405
|
await this.options.stateStore.save(this.state);
|
|
2406
|
+
if (!localContent && remoteEntry) {
|
|
2407
|
+
this.logChange("push", "delete", normalized);
|
|
2408
|
+
} else if (localContent) {
|
|
2409
|
+
this.logChange("push", remoteEntry ? "update" : "add", normalized);
|
|
2410
|
+
}
|
|
2339
2411
|
return;
|
|
2340
2412
|
}
|
|
2341
2413
|
if (!remoteContent) {
|
|
@@ -2348,6 +2420,14 @@ var SyncEngine = class {
|
|
|
2348
2420
|
updateSyncCheckpoint(this.state, normalized, remoteContent);
|
|
2349
2421
|
}
|
|
2350
2422
|
await this.options.stateStore.save(this.state);
|
|
2423
|
+
if (!remoteContent && localContent) {
|
|
2424
|
+
this.logChange("pull", "delete", normalized);
|
|
2425
|
+
} else if (remoteContent) {
|
|
2426
|
+
this.logChange("pull", localContent ? "update" : "add", normalized);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
logChange(direction, operation, path) {
|
|
2430
|
+
this.options.log(`[${direction}] ${operation} ${JSON.stringify(path)}`);
|
|
2351
2431
|
}
|
|
2352
2432
|
remoteFilePaths() {
|
|
2353
2433
|
return this.options.remote.listEntries().filter((entry) => isRemoteFile(entry) && !this.shouldIgnore(entry.path)).map((entry) => entry.path);
|
|
@@ -2391,15 +2471,15 @@ var FileSyncStateStore = class {
|
|
|
2391
2471
|
this.serialized = serialized;
|
|
2392
2472
|
}
|
|
2393
2473
|
};
|
|
2394
|
-
async function startServe(directory = process.cwd()) {
|
|
2474
|
+
async function startServe(directory = process.cwd(), options = {}) {
|
|
2395
2475
|
const root = resolve5(directory);
|
|
2396
2476
|
const settings = await readProjectSettings(root);
|
|
2397
2477
|
const session = await getServerSession(settings.serverName);
|
|
2398
2478
|
if (!session) {
|
|
2399
2479
|
throw new Error(`No login is stored for ${settings.serverName}. Run: leafrelay login ${settings.serverName}`);
|
|
2400
2480
|
}
|
|
2401
|
-
const cookie = process.env.LEAFRELAY_COOKIE || session.identity.cookies;
|
|
2402
|
-
const remote = new RemoteProject(session.url || settings.serverUrl, settings.projectId, cookie);
|
|
2481
|
+
const cookie = options.cookie || process.env.LEAFRELAY_COOKIE || session.identity.cookies;
|
|
2482
|
+
const remote = options.remote ?? new RemoteProject(session.url || settings.serverUrl, settings.projectId, cookie);
|
|
2403
2483
|
await remote.connect();
|
|
2404
2484
|
const configuredPatterns = settings.localReplica.settings["ignore-patterns"];
|
|
2405
2485
|
const ignore = createIgnoreMatcher(configuredPatterns);
|
|
@@ -2410,10 +2490,10 @@ async function startServe(directory = process.cwd()) {
|
|
|
2410
2490
|
remote,
|
|
2411
2491
|
stateStore: new FileSyncStateStore(root, settings.uri),
|
|
2412
2492
|
ignore,
|
|
2413
|
-
log,
|
|
2414
|
-
onConflict: (path, reason) => {
|
|
2415
|
-
log(`Conflict paused for ${path}: ${reason}`);
|
|
2416
|
-
}
|
|
2493
|
+
log: options.log ?? log,
|
|
2494
|
+
onConflict: options.onConflict ?? ((path, reason) => {
|
|
2495
|
+
(options.log ?? log)(`Conflict paused for ${path}: ${reason}`);
|
|
2496
|
+
})
|
|
2417
2497
|
});
|
|
2418
2498
|
try {
|
|
2419
2499
|
await engine.start();
|
|
@@ -2421,8 +2501,15 @@ async function startServe(directory = process.cwd()) {
|
|
|
2421
2501
|
remote.disconnect();
|
|
2422
2502
|
throw error2;
|
|
2423
2503
|
}
|
|
2424
|
-
log(`LeafRelay is serving ${root} (${settings.projectName}).`);
|
|
2504
|
+
(options.log ?? log)(`LeafRelay is serving ${root} (${settings.projectName}).`);
|
|
2425
2505
|
return {
|
|
2506
|
+
root,
|
|
2507
|
+
serverName: settings.serverName,
|
|
2508
|
+
projectId: settings.projectId,
|
|
2509
|
+
projectName: settings.projectName,
|
|
2510
|
+
conflicts: () => engine.getConflicts(),
|
|
2511
|
+
resolveConflict: (path, winner) => engine.resolveConflict(path, winner),
|
|
2512
|
+
retry: (path) => engine.retry(path),
|
|
2426
2513
|
stop: async () => {
|
|
2427
2514
|
await engine.stop();
|
|
2428
2515
|
remote.disconnect();
|
|
@@ -2431,39 +2518,20 @@ async function startServe(directory = process.cwd()) {
|
|
|
2431
2518
|
}
|
|
2432
2519
|
|
|
2433
2520
|
export {
|
|
2434
|
-
|
|
2435
|
-
overleafSocketProtocol,
|
|
2436
|
-
setLogSink,
|
|
2437
|
-
log,
|
|
2438
|
-
warn,
|
|
2439
|
-
error,
|
|
2440
|
-
BaseAPI,
|
|
2521
|
+
HttpRequestScheduler,
|
|
2441
2522
|
ExtendedBaseAPI,
|
|
2442
2523
|
SocketIOAPI,
|
|
2443
|
-
FolderKeys,
|
|
2444
2524
|
configPath,
|
|
2445
2525
|
normalizeServerUrl,
|
|
2446
2526
|
serverKey,
|
|
2447
2527
|
readConfig,
|
|
2448
|
-
writeConfig,
|
|
2449
2528
|
saveServerSession,
|
|
2529
|
+
saveKnownServer,
|
|
2530
|
+
removeKnownServer,
|
|
2531
|
+
removeServerSession,
|
|
2450
2532
|
getServerSession,
|
|
2451
|
-
NodeReplicaFileSystem,
|
|
2452
2533
|
readProjectSettings,
|
|
2453
|
-
DEFAULT_IGNORE_PATTERNS,
|
|
2454
|
-
createIgnoreMatcher,
|
|
2455
2534
|
RemoteProject,
|
|
2456
|
-
encodeTextBase,
|
|
2457
|
-
mergeText,
|
|
2458
|
-
SYNC_STATE_SCHEMA_VERSION,
|
|
2459
|
-
sha256,
|
|
2460
|
-
parseSyncState,
|
|
2461
|
-
createSyncState,
|
|
2462
|
-
updateSyncCheckpoint,
|
|
2463
|
-
removeSyncCheckpoint,
|
|
2464
|
-
readTextBase,
|
|
2465
|
-
SyncEngine,
|
|
2466
|
-
FileSyncStateStore,
|
|
2467
2535
|
startServe
|
|
2468
2536
|
};
|
|
2469
|
-
//# sourceMappingURL=chunk-
|
|
2537
|
+
//# sourceMappingURL=chunk-Q3R5N4YE.js.map
|