leafrelay 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-Q3R5N4YE.js → chunk-OZ4FLSVP.js} +770 -123
- package/dist/chunk-OZ4FLSVP.js.map +1 -0
- package/dist/{chunk-2EXJYAER.js → chunk-Y543FNFD.js} +11 -8
- package/dist/chunk-Y543FNFD.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/daemon.js +57 -61
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-2EXJYAER.js.map +0 -1
- package/dist/chunk-Q3R5N4YE.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
daemonStatus,
|
|
6
6
|
daemonStop,
|
|
7
7
|
serve
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-Y543FNFD.js";
|
|
9
9
|
import {
|
|
10
10
|
configPath,
|
|
11
11
|
normalizeServerUrl
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-OZ4FLSVP.js";
|
|
13
13
|
|
|
14
14
|
// src/cli.ts
|
|
15
15
|
import { Command } from "commander";
|
|
@@ -48,7 +48,7 @@ async function login(server, options) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// src/cli.ts
|
|
51
|
-
var program = new Command().name("leafrelay").description("Keep a local directory synchronized with an Overleaf project.").version("0.2.
|
|
51
|
+
var program = new Command().name("leafrelay").description("Keep a local directory synchronized with an Overleaf project.").version("0.2.4");
|
|
52
52
|
program.command("login").description("Authenticate to Overleaf and save the session in ~/.leafrelay/config.json.").argument("[server]", "Overleaf server URL", "https://www.overleaf.com/").option("--cookie <cookie>", "Overleaf browser cookie").option("--email <email>", "Overleaf account email").option("--password <password>", "Overleaf account password").action(login);
|
|
53
53
|
program.command("serve").description("Synchronize the current directory with its configured Overleaf project.").argument("[directory]", "Local replica directory", process.cwd()).action(serve);
|
|
54
54
|
var daemon = program.command("daemon").description("Inspect or control the shared LeafRelay daemon.");
|
package/dist/daemon.js
CHANGED
|
@@ -13,10 +13,10 @@ import {
|
|
|
13
13
|
saveServerSession,
|
|
14
14
|
serverKey,
|
|
15
15
|
startServe
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-OZ4FLSVP.js";
|
|
17
17
|
|
|
18
18
|
// ../daemon/src/server.ts
|
|
19
|
-
import { randomBytes } from "crypto";
|
|
19
|
+
import { randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
20
20
|
import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
21
21
|
import { createServer } from "net";
|
|
22
22
|
|
|
@@ -97,7 +97,6 @@ function decodeRpcValue(value) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// ../daemon/src/networkRuntime.ts
|
|
100
|
-
var PROJECT_IDLE_MS = 6e4;
|
|
101
100
|
var SERVER_OPERATIONS = /* @__PURE__ */ new Set([
|
|
102
101
|
"userProjectsJson",
|
|
103
102
|
"getProjectsJson",
|
|
@@ -256,10 +255,6 @@ var NetworkRuntimeRegistry = class {
|
|
|
256
255
|
async openProject(clientId, server2, projectId) {
|
|
257
256
|
const runtime = await this.getOrCreateProject(server2, projectId);
|
|
258
257
|
const shared = runtime.owners.size !== 0 || runtime.replicaReferences !== 0;
|
|
259
|
-
if (runtime.idleTimer) {
|
|
260
|
-
clearTimeout(runtime.idleTimer);
|
|
261
|
-
runtime.idleTimer = void 0;
|
|
262
|
-
}
|
|
263
258
|
runtime.owners.add(clientId);
|
|
264
259
|
await runtime.ready;
|
|
265
260
|
return { projectKey: runtime.key, shared };
|
|
@@ -294,10 +289,6 @@ var NetworkRuntimeRegistry = class {
|
|
|
294
289
|
}
|
|
295
290
|
async acquireReplica(server2, projectId) {
|
|
296
291
|
const runtime = await this.getOrCreateProject(server2, projectId);
|
|
297
|
-
if (runtime.idleTimer) {
|
|
298
|
-
clearTimeout(runtime.idleTimer);
|
|
299
|
-
runtime.idleTimer = void 0;
|
|
300
|
-
}
|
|
301
292
|
runtime.replicaReferences += 1;
|
|
302
293
|
await runtime.ready;
|
|
303
294
|
let released = false;
|
|
@@ -386,7 +377,7 @@ var NetworkRuntimeRegistry = class {
|
|
|
386
377
|
onFileRemoved: (...args) => emit("onFileRemoved", ...args),
|
|
387
378
|
onFileMoved: (...args) => emit("onFileMoved", ...args),
|
|
388
379
|
onFileChanged: (...args) => emit("onFileChanged", ...args),
|
|
389
|
-
onDisconnected: () => emit("onDisconnected"),
|
|
380
|
+
onDisconnected: (reason) => emit("onDisconnected", reason),
|
|
390
381
|
onConnectionAccepted: (...args) => emit("onConnectionAccepted", ...args),
|
|
391
382
|
onClientUpdated: (...args) => emit("onClientUpdated", ...args),
|
|
392
383
|
onClientDisconnected: (...args) => emit("onClientDisconnected", ...args),
|
|
@@ -416,7 +407,8 @@ var NetworkRuntimeRegistry = class {
|
|
|
416
407
|
remove: (path) => this.enqueueProjectOperation(runtime, () => runtime.remote.remove(path)),
|
|
417
408
|
getCurrentVersion: () => this.enqueueProjectOperation(runtime, () => runtime.remote.getCurrentVersion()),
|
|
418
409
|
getFileTreeDiff: (from, to) => this.enqueueProjectOperation(runtime, () => runtime.remote.getFileTreeDiff(from, to)),
|
|
419
|
-
onChange: (handler) => runtime.remote.onChange(handler)
|
|
410
|
+
onChange: (handler) => runtime.remote.onChange(handler),
|
|
411
|
+
onReconnect: (handler) => runtime.remote.onReconnect(handler)
|
|
420
412
|
};
|
|
421
413
|
}
|
|
422
414
|
enqueueProjectOperation(runtime, operation) {
|
|
@@ -425,19 +417,15 @@ var NetworkRuntimeRegistry = class {
|
|
|
425
417
|
return result;
|
|
426
418
|
}
|
|
427
419
|
scheduleProjectStop(runtime) {
|
|
428
|
-
if (runtime.owners.size !== 0 || runtime.replicaReferences !== 0
|
|
420
|
+
if (runtime.owners.size !== 0 || runtime.replicaReferences !== 0) {
|
|
429
421
|
return;
|
|
430
422
|
}
|
|
431
|
-
|
|
432
|
-
runtime.idleTimer.unref();
|
|
423
|
+
void this.stopProject(runtime);
|
|
433
424
|
}
|
|
434
425
|
async stopProject(runtime) {
|
|
435
426
|
if (!this.projects.delete(runtime.key)) {
|
|
436
427
|
return;
|
|
437
428
|
}
|
|
438
|
-
if (runtime.idleTimer) {
|
|
439
|
-
clearTimeout(runtime.idleTimer);
|
|
440
|
-
}
|
|
441
429
|
runtime.socket.disconnect();
|
|
442
430
|
}
|
|
443
431
|
};
|
|
@@ -446,7 +434,6 @@ var NetworkRuntimeRegistry = class {
|
|
|
446
434
|
import { randomUUID } from "crypto";
|
|
447
435
|
import { realpath } from "fs/promises";
|
|
448
436
|
import { resolve as resolve2 } from "path";
|
|
449
|
-
var DETACHED_REPLICA_GRACE_MS = 6e4;
|
|
450
437
|
var ReplicaAlreadyActiveError = class extends Error {
|
|
451
438
|
constructor(projectId, activeRoot, requestedRoot) {
|
|
452
439
|
super(`Overleaf project ${projectId} is already synchronized by ${activeRoot}; ${requestedRoot} cannot become a second writable replica.`);
|
|
@@ -470,9 +457,13 @@ var ReplicaRegistry = class {
|
|
|
470
457
|
byRoot = /* @__PURE__ */ new Map();
|
|
471
458
|
byProject = /* @__PURE__ */ new Map();
|
|
472
459
|
projectOperations = /* @__PURE__ */ new Map();
|
|
460
|
+
clients = /* @__PURE__ */ new Set();
|
|
473
461
|
get size() {
|
|
474
462
|
return this.byId.size;
|
|
475
463
|
}
|
|
464
|
+
registerClient(clientId) {
|
|
465
|
+
this.clients.add(clientId);
|
|
466
|
+
}
|
|
476
467
|
async attach(clientId, params) {
|
|
477
468
|
const root = await realpath(resolve2(params.directory));
|
|
478
469
|
const settings = await readProjectSettings(root);
|
|
@@ -480,6 +471,9 @@ var ReplicaRegistry = class {
|
|
|
480
471
|
return this.withProjectLock(projectKey, () => this.attachResolved(clientId, params, root, settings, projectKey));
|
|
481
472
|
}
|
|
482
473
|
async attachResolved(clientId, params, root, settings, projectKey) {
|
|
474
|
+
if (!this.clients.has(clientId)) {
|
|
475
|
+
throw new Error("The replica client disconnected before attachment started.");
|
|
476
|
+
}
|
|
483
477
|
const rootEntry = this.byRoot.get(root);
|
|
484
478
|
if (rootEntry) {
|
|
485
479
|
if (rootEntry.projectKey !== projectKey) {
|
|
@@ -515,6 +509,11 @@ var ReplicaRegistry = class {
|
|
|
515
509
|
this.events.status(entry.owners, { replicaId: entry.id, status: entry.status });
|
|
516
510
|
}
|
|
517
511
|
});
|
|
512
|
+
if (!this.clients.has(clientId)) {
|
|
513
|
+
await running.stop();
|
|
514
|
+
this.events.empty();
|
|
515
|
+
throw new Error("The replica client disconnected before attachment completed.");
|
|
516
|
+
}
|
|
518
517
|
entry = {
|
|
519
518
|
id,
|
|
520
519
|
root,
|
|
@@ -537,17 +536,24 @@ var ReplicaRegistry = class {
|
|
|
537
536
|
throw error;
|
|
538
537
|
}
|
|
539
538
|
}
|
|
540
|
-
detach(clientId, replicaId) {
|
|
539
|
+
async detach(clientId, replicaId) {
|
|
541
540
|
const entry = this.require(replicaId);
|
|
542
|
-
entry.
|
|
543
|
-
|
|
541
|
+
await this.withProjectLock(entry.projectKey, async () => {
|
|
542
|
+
entry.owners.delete(clientId);
|
|
543
|
+
if (entry.owners.size === 0) {
|
|
544
|
+
await this.stop(entry);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
544
547
|
}
|
|
545
|
-
detachClient(clientId) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
548
|
+
async detachClient(clientId) {
|
|
549
|
+
this.clients.delete(clientId);
|
|
550
|
+
const owned = [...this.byId.values()].filter((entry) => entry.owners.has(clientId));
|
|
551
|
+
await Promise.all(owned.map((entry) => this.withProjectLock(entry.projectKey, async () => {
|
|
552
|
+
entry.owners.delete(clientId);
|
|
553
|
+
if (entry.owners.size === 0) {
|
|
554
|
+
await this.stop(entry);
|
|
549
555
|
}
|
|
550
|
-
}
|
|
556
|
+
})));
|
|
551
557
|
}
|
|
552
558
|
async resolveConflict(replicaId, path, winner) {
|
|
553
559
|
const entry = this.require(replicaId);
|
|
@@ -577,26 +583,12 @@ var ReplicaRegistry = class {
|
|
|
577
583
|
await Promise.all([...this.byId.values()].map((entry) => this.stop(entry)));
|
|
578
584
|
}
|
|
579
585
|
addOwner(entry, clientId) {
|
|
580
|
-
if (entry.stopTimer) {
|
|
581
|
-
clearTimeout(entry.stopTimer);
|
|
582
|
-
entry.stopTimer = void 0;
|
|
583
|
-
}
|
|
584
586
|
entry.owners.add(clientId);
|
|
585
587
|
}
|
|
586
|
-
scheduleStopWhenDetached(entry) {
|
|
587
|
-
if (entry.owners.size !== 0 || entry.stopTimer) {
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
entry.stopTimer = setTimeout(() => void this.stop(entry), DETACHED_REPLICA_GRACE_MS);
|
|
591
|
-
entry.stopTimer.unref();
|
|
592
|
-
}
|
|
593
588
|
async stop(entry) {
|
|
594
589
|
if (!this.byId.delete(entry.id)) {
|
|
595
590
|
return;
|
|
596
591
|
}
|
|
597
|
-
if (entry.stopTimer) {
|
|
598
|
-
clearTimeout(entry.stopTimer);
|
|
599
|
-
}
|
|
600
592
|
this.byRoot.delete(entry.root);
|
|
601
593
|
this.byProject.delete(entry.projectKey);
|
|
602
594
|
entry.status = { state: "stopping" };
|
|
@@ -656,17 +648,17 @@ var CLOSED_SOCKET_ERRORS = /* @__PURE__ */ new Set([
|
|
|
656
648
|
"ERR_STREAM_WRITE_AFTER_END"
|
|
657
649
|
]);
|
|
658
650
|
var LeafRelaySocketWriter = class extends SocketMessageWriter {
|
|
659
|
-
constructor(
|
|
660
|
-
super(
|
|
661
|
-
this.
|
|
651
|
+
constructor(transportSocket) {
|
|
652
|
+
super(transportSocket);
|
|
653
|
+
this.transportSocket = transportSocket;
|
|
662
654
|
}
|
|
663
|
-
|
|
655
|
+
transportSocket;
|
|
664
656
|
async write(message) {
|
|
665
657
|
try {
|
|
666
658
|
await super.write(message);
|
|
667
659
|
} catch (error) {
|
|
668
660
|
const code = error.code;
|
|
669
|
-
if (this.
|
|
661
|
+
if (this.transportSocket.destroyed || this.transportSocket.writableEnded || code && CLOSED_SOCKET_ERRORS.has(code)) {
|
|
670
662
|
return;
|
|
671
663
|
}
|
|
672
664
|
throw error;
|
|
@@ -680,7 +672,7 @@ function createSocketRpcConnection(socket) {
|
|
|
680
672
|
// ../daemon/src/server.ts
|
|
681
673
|
var IDLE_EXIT_MS = 6e4;
|
|
682
674
|
var REPLICA_ALREADY_ACTIVE = -32010;
|
|
683
|
-
var daemonVersion = true ? "0.2.
|
|
675
|
+
var daemonVersion = true ? "0.2.4" : "development";
|
|
684
676
|
var LeafRelayDaemonServer = class {
|
|
685
677
|
constructor(options = {}) {
|
|
686
678
|
this.options = options;
|
|
@@ -775,7 +767,7 @@ var LeafRelayDaemonServer = class {
|
|
|
775
767
|
accept(socket) {
|
|
776
768
|
this.cancelIdleExit();
|
|
777
769
|
const rpc = createSocketRpcConnection(socket);
|
|
778
|
-
const client = { socket, rpc, initialized: false };
|
|
770
|
+
const client = { leaseId: randomUUID2(), socket, rpc, initialized: false };
|
|
779
771
|
this.clients.add(client);
|
|
780
772
|
rpc.onRequest(RPC_METHOD.initialize, (params) => this.initialize(client, params));
|
|
781
773
|
rpc.onRequest(RPC_METHOD.daemonStatus, () => this.authorized(client, () => this.status()));
|
|
@@ -785,7 +777,7 @@ var LeafRelayDaemonServer = class {
|
|
|
785
777
|
}));
|
|
786
778
|
rpc.onRequest(RPC_METHOD.replicaAttach, (params) => this.authorized(client, async () => {
|
|
787
779
|
try {
|
|
788
|
-
return await this.replicas.attach(client.
|
|
780
|
+
return await this.replicas.attach(client.leaseId, params);
|
|
789
781
|
} catch (error) {
|
|
790
782
|
if (error instanceof ReplicaAlreadyActiveError) {
|
|
791
783
|
throw new ResponseError(REPLICA_ALREADY_ACTIVE, error.message, {
|
|
@@ -798,8 +790,8 @@ var LeafRelayDaemonServer = class {
|
|
|
798
790
|
throw error;
|
|
799
791
|
}
|
|
800
792
|
}));
|
|
801
|
-
rpc.onRequest(RPC_METHOD.replicaDetach, (params) => this.authorized(client, () => {
|
|
802
|
-
this.replicas.detach(client.
|
|
793
|
+
rpc.onRequest(RPC_METHOD.replicaDetach, (params) => this.authorized(client, async () => {
|
|
794
|
+
await this.replicas.detach(client.leaseId, params.replicaId);
|
|
803
795
|
return null;
|
|
804
796
|
}));
|
|
805
797
|
rpc.onRequest(RPC_METHOD.replicaResolveConflict, (params) => this.authorized(client, async () => {
|
|
@@ -829,16 +821,16 @@ var LeafRelayDaemonServer = class {
|
|
|
829
821
|
const result = await this.network.callServer(params.server, params.operation, decodeRpcValue(params.args));
|
|
830
822
|
return encodeRpcValue(result);
|
|
831
823
|
}));
|
|
832
|
-
rpc.onRequest(RPC_METHOD.projectOpen, (params) => this.authorized(client, () => this.network.openProject(client.
|
|
824
|
+
rpc.onRequest(RPC_METHOD.projectOpen, (params) => this.authorized(client, () => this.network.openProject(client.leaseId, params.server, params.projectId)));
|
|
833
825
|
rpc.onRequest(RPC_METHOD.projectClose, (params) => this.authorized(client, () => {
|
|
834
|
-
this.network.closeProject(client.
|
|
826
|
+
this.network.closeProject(client.leaseId, params.projectKey);
|
|
835
827
|
return null;
|
|
836
828
|
}));
|
|
837
829
|
rpc.onRequest(RPC_METHOD.projectCall, (params) => this.authorized(client, async () => {
|
|
838
830
|
const result = await this.network.callProject(params.projectKey, params.operation, decodeRpcValue(params.args));
|
|
839
831
|
return encodeRpcValue(await result);
|
|
840
832
|
}));
|
|
841
|
-
rpc.onClose(() => this.closeClient(client));
|
|
833
|
+
rpc.onClose(() => void this.closeClient(client));
|
|
842
834
|
rpc.onError((error) => void this.log("error", `JSON-RPC connection error: ${String(error[0])}`));
|
|
843
835
|
rpc.listen();
|
|
844
836
|
}
|
|
@@ -854,6 +846,7 @@ var LeafRelayDaemonServer = class {
|
|
|
854
846
|
}
|
|
855
847
|
client.clientId = params.clientId;
|
|
856
848
|
client.initialized = true;
|
|
849
|
+
this.replicas.registerClient(client.leaseId);
|
|
857
850
|
void this.log("info", `${params.clientName} ${params.clientVersion} connected (${params.clientId}).`);
|
|
858
851
|
return {
|
|
859
852
|
daemonVersion,
|
|
@@ -868,21 +861,24 @@ var LeafRelayDaemonServer = class {
|
|
|
868
861
|
}
|
|
869
862
|
return operation();
|
|
870
863
|
}
|
|
871
|
-
closeClient(client) {
|
|
864
|
+
async closeClient(client) {
|
|
872
865
|
if (!this.clients.delete(client)) {
|
|
873
866
|
return;
|
|
874
867
|
}
|
|
875
868
|
if (client.clientId) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
869
|
+
try {
|
|
870
|
+
await this.replicas.detachClient(client.leaseId);
|
|
871
|
+
} catch (error) {
|
|
872
|
+
await this.log("error", `Failed to stop replicas for a disconnected client: ${error instanceof Error ? error.message : String(error)}`);
|
|
873
|
+
} finally {
|
|
874
|
+
this.network.detachClient(client.leaseId);
|
|
875
|
+
}
|
|
880
876
|
}
|
|
881
877
|
this.scheduleIdleExit();
|
|
882
878
|
}
|
|
883
879
|
notifyOwners(owners, method, notification) {
|
|
884
880
|
for (const client of this.clients) {
|
|
885
|
-
if (
|
|
881
|
+
if (owners.has(client.leaseId)) {
|
|
886
882
|
this.notifyClient(client, method, notification);
|
|
887
883
|
}
|
|
888
884
|
}
|