leafrelay 0.2.3 → 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-3RL67IG5.js → chunk-OZ4FLSVP.js} +652 -89
- package/dist/chunk-OZ4FLSVP.js.map +1 -0
- package/dist/{chunk-XXR6LFT7.js → chunk-Y543FNFD.js} +6 -3
- package/dist/chunk-Y543FNFD.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/daemon.js +49 -54
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/dist/chunk-3RL67IG5.js.map +0 -1
- package/dist/chunk-XXR6LFT7.js.map +0 -1
|
@@ -18,10 +18,12 @@ import { createHash } from "crypto";
|
|
|
18
18
|
import { posix } from "path";
|
|
19
19
|
import DiffMatchPatch from "diff-match-patch";
|
|
20
20
|
import { posix as posix2 } from "path";
|
|
21
|
+
import DiffMatchPatch2 from "diff-match-patch";
|
|
21
22
|
import { createHash as createHash2 } from "crypto";
|
|
22
23
|
import { diff3Merge } from "node-diff3";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
24
|
+
import { randomUUID } from "crypto";
|
|
25
|
+
import { mkdir as mkdir3, readFile as readFile4, readdir as readdir2, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
26
|
+
import { dirname as dirname3, join as join2, resolve as resolve4 } from "path";
|
|
25
27
|
var PACKET_TYPES = [
|
|
26
28
|
"disconnect",
|
|
27
29
|
"connect",
|
|
@@ -1828,6 +1830,7 @@ var RemoteProject = class {
|
|
|
1828
1830
|
project;
|
|
1829
1831
|
entries = /* @__PURE__ */ new Map();
|
|
1830
1832
|
idToPath = /* @__PURE__ */ new Map();
|
|
1833
|
+
collaboratorNames = /* @__PURE__ */ new Map();
|
|
1831
1834
|
changeHandler;
|
|
1832
1835
|
reconnectHandler;
|
|
1833
1836
|
externalTransport;
|
|
@@ -1844,6 +1847,7 @@ var RemoteProject = class {
|
|
|
1844
1847
|
this.socket = new SocketIOAPI(this.url, this.api, this.identity, this.projectId);
|
|
1845
1848
|
}
|
|
1846
1849
|
this.project = await this.socket.joinProject(this.projectId);
|
|
1850
|
+
this.indexCollaborators();
|
|
1847
1851
|
this.reindex();
|
|
1848
1852
|
this.registerEvents();
|
|
1849
1853
|
}
|
|
@@ -1854,9 +1858,19 @@ var RemoteProject = class {
|
|
|
1854
1858
|
}
|
|
1855
1859
|
onChange(handler) {
|
|
1856
1860
|
this.changeHandler = handler;
|
|
1861
|
+
return () => {
|
|
1862
|
+
if (this.changeHandler === handler) {
|
|
1863
|
+
this.changeHandler = void 0;
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1857
1866
|
}
|
|
1858
1867
|
onReconnect(handler) {
|
|
1859
1868
|
this.reconnectHandler = handler;
|
|
1869
|
+
return () => {
|
|
1870
|
+
if (this.reconnectHandler === handler) {
|
|
1871
|
+
this.reconnectHandler = void 0;
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1860
1874
|
}
|
|
1861
1875
|
listEntries() {
|
|
1862
1876
|
return [...this.entries.values()];
|
|
@@ -2071,16 +2085,40 @@ var RemoteProject = class {
|
|
|
2071
2085
|
collection.splice(index, 1);
|
|
2072
2086
|
}
|
|
2073
2087
|
}
|
|
2074
|
-
emitChange(path, previousPath) {
|
|
2075
|
-
this.changeHandler?.({ path: normalizePath(path), previousPath });
|
|
2088
|
+
emitChange(path, previousPath, actor) {
|
|
2089
|
+
this.changeHandler?.({ path: normalizePath(path), previousPath, actor });
|
|
2090
|
+
}
|
|
2091
|
+
indexCollaborators() {
|
|
2092
|
+
for (const member of [this.project.owner, ...this.project.members]) {
|
|
2093
|
+
if (!member?._id) {
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
const name = [member.first_name, member.last_name].filter(Boolean).join(" ").trim();
|
|
2097
|
+
if (name) {
|
|
2098
|
+
this.collaboratorNames.set(member._id, name);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
actor(userId) {
|
|
2103
|
+
if (!userId) {
|
|
2104
|
+
return void 0;
|
|
2105
|
+
}
|
|
2106
|
+
const name = this.collaboratorNames.get(userId);
|
|
2107
|
+
return name ? `${name} (Overleaf)` : `Overleaf user ${userId}`;
|
|
2076
2108
|
}
|
|
2077
2109
|
registerEvents() {
|
|
2078
2110
|
this.socket.updateEventHandlers({
|
|
2079
2111
|
onProjectJoined: (project) => {
|
|
2080
2112
|
this.project = project;
|
|
2113
|
+
this.indexCollaborators();
|
|
2081
2114
|
this.reindex();
|
|
2082
2115
|
this.reconnectHandler?.();
|
|
2083
2116
|
},
|
|
2117
|
+
onClientUpdated: (user) => {
|
|
2118
|
+
if (user.user_id && user.name) {
|
|
2119
|
+
this.collaboratorNames.set(user.user_id, user.name);
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2084
2122
|
onFileCreated: (parentId, type, entity) => {
|
|
2085
2123
|
const parent = this.folderById(parentId);
|
|
2086
2124
|
if (!parent || type === "outputs") {
|
|
@@ -2145,12 +2183,121 @@ var RemoteProject = class {
|
|
|
2145
2183
|
onFileChanged: (update) => {
|
|
2146
2184
|
const path = this.idToPath.get(update.doc);
|
|
2147
2185
|
if (path) {
|
|
2148
|
-
this.emitChange(path);
|
|
2186
|
+
this.emitChange(path, void 0, this.actor(update.meta?.user_id));
|
|
2149
2187
|
}
|
|
2150
2188
|
}
|
|
2151
2189
|
});
|
|
2152
2190
|
}
|
|
2153
2191
|
};
|
|
2192
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2193
|
+
function decode(content) {
|
|
2194
|
+
if (content === void 0) {
|
|
2195
|
+
return "";
|
|
2196
|
+
}
|
|
2197
|
+
if (content.includes(0)) {
|
|
2198
|
+
return void 0;
|
|
2199
|
+
}
|
|
2200
|
+
try {
|
|
2201
|
+
return decoder.decode(content);
|
|
2202
|
+
} catch {
|
|
2203
|
+
return void 0;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
function lineStarts(content) {
|
|
2207
|
+
const starts = [0];
|
|
2208
|
+
for (let index = 0; index < content.length; index++) {
|
|
2209
|
+
if (content[index] === "\n" && index + 1 < content.length) {
|
|
2210
|
+
starts.push(index + 1);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
return starts;
|
|
2214
|
+
}
|
|
2215
|
+
function lineAt(starts, offset) {
|
|
2216
|
+
let low = 0;
|
|
2217
|
+
let high = starts.length;
|
|
2218
|
+
while (low < high) {
|
|
2219
|
+
const middle = Math.floor((low + high) / 2);
|
|
2220
|
+
if (starts[middle] <= offset) {
|
|
2221
|
+
low = middle + 1;
|
|
2222
|
+
} else {
|
|
2223
|
+
high = middle;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return Math.max(1, low);
|
|
2227
|
+
}
|
|
2228
|
+
function toLineRange(content, start, end) {
|
|
2229
|
+
const starts = lineStarts(content);
|
|
2230
|
+
const safeStart = Math.min(start, Math.max(0, content.length - 1));
|
|
2231
|
+
const safeEnd = Math.min(Math.max(start, end - 1), Math.max(0, content.length - 1));
|
|
2232
|
+
return { start: lineAt(starts, safeStart), end: lineAt(starts, safeEnd) };
|
|
2233
|
+
}
|
|
2234
|
+
function mergeRanges(ranges) {
|
|
2235
|
+
const merged = [];
|
|
2236
|
+
for (const range of ranges.sort((a, b) => a.start - b.start)) {
|
|
2237
|
+
const previous = merged.at(-1);
|
|
2238
|
+
if (previous && range.start <= previous.end + 1) {
|
|
2239
|
+
previous.end = Math.max(previous.end, range.end);
|
|
2240
|
+
} else {
|
|
2241
|
+
merged.push({ ...range });
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return merged;
|
|
2245
|
+
}
|
|
2246
|
+
function formatRanges(ranges) {
|
|
2247
|
+
const shown = ranges.slice(0, 3).map((range) => range.start === range.end ? String(range.start) : `${range.start}-${range.end}`);
|
|
2248
|
+
return `${shown.join(", ")}${ranges.length > shown.length ? ` (+${ranges.length - shown.length} more)` : ""}`;
|
|
2249
|
+
}
|
|
2250
|
+
function describeContentChange(before, after) {
|
|
2251
|
+
if (before && after && Buffer.from(before).equals(Buffer.from(after))) {
|
|
2252
|
+
return void 0;
|
|
2253
|
+
}
|
|
2254
|
+
const oldText = decode(before);
|
|
2255
|
+
const newText = decode(after);
|
|
2256
|
+
if (oldText === void 0 || newText === void 0) {
|
|
2257
|
+
return `binary bytes ${before?.byteLength ?? 0} -> ${after?.byteLength ?? 0}`;
|
|
2258
|
+
}
|
|
2259
|
+
if (oldText.length === 0 && newText.length === 0) {
|
|
2260
|
+
return void 0;
|
|
2261
|
+
}
|
|
2262
|
+
if (oldText.length === 0) {
|
|
2263
|
+
const count = lineStarts(newText).length;
|
|
2264
|
+
return `added lines ${count === 1 ? "1" : `1-${count}`}`;
|
|
2265
|
+
}
|
|
2266
|
+
if (newText.length === 0) {
|
|
2267
|
+
const count = lineStarts(oldText).length;
|
|
2268
|
+
return `removed lines ${count === 1 ? "1" : `1-${count}`}`;
|
|
2269
|
+
}
|
|
2270
|
+
const changes = [];
|
|
2271
|
+
let oldOffset = 0;
|
|
2272
|
+
let newOffset = 0;
|
|
2273
|
+
let current;
|
|
2274
|
+
const finish = () => {
|
|
2275
|
+
if (current) {
|
|
2276
|
+
changes.push(current);
|
|
2277
|
+
current = void 0;
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
for (const [operation, value] of new DiffMatchPatch2().diff_main(oldText, newText)) {
|
|
2281
|
+
if (operation === 0) {
|
|
2282
|
+
finish();
|
|
2283
|
+
oldOffset += value.length;
|
|
2284
|
+
newOffset += value.length;
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
current ??= { oldStart: oldOffset, oldEnd: oldOffset, newStart: newOffset, newEnd: newOffset };
|
|
2288
|
+
if (operation === -1) {
|
|
2289
|
+
oldOffset += value.length;
|
|
2290
|
+
}
|
|
2291
|
+
if (operation === 1) {
|
|
2292
|
+
newOffset += value.length;
|
|
2293
|
+
}
|
|
2294
|
+
current.oldEnd = oldOffset;
|
|
2295
|
+
current.newEnd = newOffset;
|
|
2296
|
+
}
|
|
2297
|
+
finish();
|
|
2298
|
+
const ranges = mergeRanges(changes.map((change) => change.newEnd > change.newStart ? toLineRange(newText, change.newStart, change.newEnd) : toLineRange(oldText, change.oldStart, change.oldEnd)));
|
|
2299
|
+
return ranges.length === 0 ? void 0 : `lines ${formatRanges(ranges)}`;
|
|
2300
|
+
}
|
|
2154
2301
|
var textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
2155
2302
|
var textEncoder = new TextEncoder();
|
|
2156
2303
|
function decodeText(content) {
|
|
@@ -2190,32 +2337,19 @@ function mergeText(baseContent, localContent, remoteContent) {
|
|
|
2190
2337
|
content: textEncoder.encode(regions.flatMap((region) => region.ok ?? []).join(""))
|
|
2191
2338
|
};
|
|
2192
2339
|
}
|
|
2193
|
-
var SYNC_STATE_SCHEMA_VERSION =
|
|
2340
|
+
var SYNC_STATE_SCHEMA_VERSION = 1;
|
|
2194
2341
|
function sha256(content) {
|
|
2195
2342
|
return createHash2("sha256").update(content).digest("hex");
|
|
2196
2343
|
}
|
|
2197
|
-
function
|
|
2198
|
-
try {
|
|
2199
|
-
const state = JSON.parse(serialized);
|
|
2200
|
-
if (state.schemaVersion !== 1 && state.schemaVersion !== SYNC_STATE_SCHEMA_VERSION || state.projectUri !== projectUri || !Number.isInteger(state.remoteVersion) || typeof state.files !== "object" || state.files === null) {
|
|
2201
|
-
return void 0;
|
|
2202
|
-
}
|
|
2203
|
-
return {
|
|
2204
|
-
...state,
|
|
2205
|
-
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
2206
|
-
textBases: typeof state.textBases === "object" && state.textBases !== null ? state.textBases : {}
|
|
2207
|
-
};
|
|
2208
|
-
} catch {
|
|
2209
|
-
return void 0;
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
function createSyncState(projectUri, remoteVersion) {
|
|
2344
|
+
function createSyncState(projectUri, remoteVersion, initialized = true) {
|
|
2213
2345
|
return {
|
|
2214
2346
|
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
2215
2347
|
projectUri,
|
|
2216
2348
|
remoteVersion,
|
|
2349
|
+
initialized,
|
|
2217
2350
|
files: {},
|
|
2218
|
-
textBases: {}
|
|
2351
|
+
textBases: {},
|
|
2352
|
+
conflicts: {}
|
|
2219
2353
|
};
|
|
2220
2354
|
}
|
|
2221
2355
|
function updateSyncCheckpoint(state, path, content) {
|
|
@@ -2228,10 +2362,12 @@ function updateSyncCheckpoint(state, path, content) {
|
|
|
2228
2362
|
}
|
|
2229
2363
|
}
|
|
2230
2364
|
function removeSyncCheckpoint(state, path) {
|
|
2231
|
-
|
|
2365
|
+
const checkpointPaths = /* @__PURE__ */ new Set([...Object.keys(state.files), ...Object.keys(state.conflicts)]);
|
|
2366
|
+
for (const checkpointPath of checkpointPaths) {
|
|
2232
2367
|
if (checkpointPath === path || checkpointPath.startsWith(`${path}/`)) {
|
|
2233
2368
|
delete state.files[checkpointPath];
|
|
2234
2369
|
delete state.textBases[checkpointPath];
|
|
2370
|
+
delete state.conflicts[checkpointPath];
|
|
2235
2371
|
}
|
|
2236
2372
|
}
|
|
2237
2373
|
}
|
|
@@ -2254,34 +2390,36 @@ var SyncEngine = class {
|
|
|
2254
2390
|
queue = Promise.resolve();
|
|
2255
2391
|
conflicts = /* @__PURE__ */ new Set();
|
|
2256
2392
|
stopLocalWatch;
|
|
2393
|
+
stopRemoteChange;
|
|
2394
|
+
stopRemoteReconnect;
|
|
2257
2395
|
eventTimers = /* @__PURE__ */ new Map();
|
|
2396
|
+
stopped = false;
|
|
2258
2397
|
async start() {
|
|
2398
|
+
this.stopped = false;
|
|
2259
2399
|
this.state = await this.options.stateStore.load();
|
|
2260
2400
|
const currentVersion = await this.options.remote.getCurrentVersion();
|
|
2261
2401
|
const localFiles = await this.options.local.listFiles();
|
|
2262
2402
|
const remoteFiles = this.remoteFilePaths();
|
|
2263
|
-
if (!this.state) {
|
|
2264
|
-
this.
|
|
2265
|
-
if (localFiles.length !== 0 && remoteFiles.length !== 0) {
|
|
2266
|
-
throw new Error("Synchronization paused: both the local and Overleaf projects contain files, but no common checkpoint exists.");
|
|
2267
|
-
}
|
|
2268
|
-
const source = localFiles.length === 0 ? "remote" : "local";
|
|
2269
|
-
for (const path of source === "remote" ? remoteFiles : localFiles) {
|
|
2270
|
-
await this.reconcilePath(path, source);
|
|
2271
|
-
}
|
|
2272
|
-
this.state.remoteVersion = currentVersion;
|
|
2273
|
-
await this.options.stateStore.save(this.state);
|
|
2403
|
+
if (!this.state || !this.state.initialized) {
|
|
2404
|
+
await this.initializeState(currentVersion, localFiles, remoteFiles);
|
|
2274
2405
|
} else {
|
|
2406
|
+
this.restoreConflicts();
|
|
2275
2407
|
await this.startupReconcile(currentVersion, localFiles, remoteFiles);
|
|
2276
2408
|
}
|
|
2277
2409
|
this.stopLocalWatch = await this.options.local.watch((path) => this.schedule(path, "local"));
|
|
2278
|
-
this.options.remote.onChange((change) => {
|
|
2410
|
+
this.stopRemoteChange = this.options.remote.onChange((change) => {
|
|
2411
|
+
if (this.stopped) {
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2279
2414
|
if (change.previousPath) {
|
|
2280
|
-
this.schedule(change.previousPath, "remote");
|
|
2415
|
+
this.schedule(change.previousPath, "remote", change.actor);
|
|
2281
2416
|
}
|
|
2282
|
-
this.schedule(change.path, "remote");
|
|
2417
|
+
this.schedule(change.path, "remote", change.actor);
|
|
2283
2418
|
});
|
|
2284
|
-
this.options.remote.onReconnect?.(() => {
|
|
2419
|
+
this.stopRemoteReconnect = this.options.remote.onReconnect?.(() => {
|
|
2420
|
+
if (this.stopped) {
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2285
2423
|
this.queue = this.queue.then(() => this.reconcileAfterReconnect()).catch((error2) => {
|
|
2286
2424
|
this.options.log(`Failed to reconcile after reconnecting: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
2287
2425
|
});
|
|
@@ -2289,11 +2427,17 @@ var SyncEngine = class {
|
|
|
2289
2427
|
this.options.log(`Synchronization active at Overleaf version ${this.state.remoteVersion}.`);
|
|
2290
2428
|
}
|
|
2291
2429
|
async stop() {
|
|
2292
|
-
|
|
2293
|
-
|
|
2430
|
+
this.stopped = true;
|
|
2431
|
+
this.stopRemoteChange?.();
|
|
2432
|
+
this.stopRemoteChange = void 0;
|
|
2433
|
+
this.stopRemoteReconnect?.();
|
|
2434
|
+
this.stopRemoteReconnect = void 0;
|
|
2435
|
+
for (const pending of this.eventTimers.values()) {
|
|
2436
|
+
clearTimeout(pending.timer);
|
|
2294
2437
|
}
|
|
2295
2438
|
this.eventTimers.clear();
|
|
2296
2439
|
await this.stopLocalWatch?.();
|
|
2440
|
+
this.stopLocalWatch = void 0;
|
|
2297
2441
|
await this.queue;
|
|
2298
2442
|
}
|
|
2299
2443
|
getConflicts() {
|
|
@@ -2329,15 +2473,95 @@ var SyncEngine = class {
|
|
|
2329
2473
|
await this.options.local.remove(normalized);
|
|
2330
2474
|
removeSyncCheckpoint(this.state, normalized);
|
|
2331
2475
|
}
|
|
2332
|
-
await this.options.stateStore.save(this.state);
|
|
2333
2476
|
this.conflicts.delete(normalized);
|
|
2477
|
+
delete this.state.conflicts[normalized];
|
|
2478
|
+
await this.options.stateStore.save(this.state);
|
|
2334
2479
|
this.options.log(`Resolved ${normalized} using ${winner === "local" ? "local" : "Overleaf"} content.`);
|
|
2335
2480
|
});
|
|
2336
2481
|
}
|
|
2337
2482
|
async retry(path) {
|
|
2338
2483
|
const normalized = normalizePath2(path);
|
|
2484
|
+
const reason = this.state.conflicts[normalized];
|
|
2339
2485
|
this.conflicts.delete(normalized);
|
|
2340
|
-
|
|
2486
|
+
delete this.state.conflicts[normalized];
|
|
2487
|
+
try {
|
|
2488
|
+
await this.enqueue(() => this.reconcilePath(normalized, "local"));
|
|
2489
|
+
} catch (error2) {
|
|
2490
|
+
if (reason !== void 0) {
|
|
2491
|
+
this.conflicts.add(normalized);
|
|
2492
|
+
this.state.conflicts[normalized] = reason;
|
|
2493
|
+
await this.options.stateStore.save(this.state);
|
|
2494
|
+
}
|
|
2495
|
+
throw error2;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
async initializeState(currentVersion, localFiles, remoteFiles) {
|
|
2499
|
+
if (!this.state) {
|
|
2500
|
+
this.state = createSyncState(this.options.projectUri, currentVersion, false);
|
|
2501
|
+
await this.options.stateStore.save(this.state);
|
|
2502
|
+
}
|
|
2503
|
+
this.restoreConflicts();
|
|
2504
|
+
const paths = /* @__PURE__ */ new Set([...localFiles, ...remoteFiles, ...Object.keys(this.state.files)]);
|
|
2505
|
+
for (const path of [...paths].sort()) {
|
|
2506
|
+
const normalized = normalizePath2(path);
|
|
2507
|
+
if (this.conflicts.has(normalized)) {
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
if (this.state.files[normalized] !== void 0) {
|
|
2511
|
+
await this.reconcilePath(normalized, "local");
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
await this.initializePath(normalized);
|
|
2515
|
+
}
|
|
2516
|
+
this.state.remoteVersion = currentVersion;
|
|
2517
|
+
this.state.initialized = true;
|
|
2518
|
+
await this.options.stateStore.save(this.state);
|
|
2519
|
+
if (this.conflicts.size !== 0) {
|
|
2520
|
+
this.options.log(`Synchronization remains active; ${this.conflicts.size} conflicting path(s) are paused.`);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
async initializePath(path) {
|
|
2524
|
+
const remoteEntry = this.options.remote.entry(path);
|
|
2525
|
+
if (remoteEntry?.type === "folder") {
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
const localContent = await this.options.local.read(path);
|
|
2529
|
+
const remoteContent = isRemoteFile(remoteEntry) ? await this.options.remote.read(path) : void 0;
|
|
2530
|
+
const localHash = localContent && sha256(localContent);
|
|
2531
|
+
const remoteHash = remoteContent && sha256(remoteContent);
|
|
2532
|
+
if (localHash === remoteHash) {
|
|
2533
|
+
if (localContent) {
|
|
2534
|
+
updateSyncCheckpoint(this.state, path, localContent);
|
|
2535
|
+
await this.options.stateStore.save(this.state);
|
|
2536
|
+
}
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (!localContent && remoteContent) {
|
|
2540
|
+
await this.options.local.write(path, remoteContent);
|
|
2541
|
+
updateSyncCheckpoint(this.state, path, remoteContent);
|
|
2542
|
+
await this.options.stateStore.save(this.state);
|
|
2543
|
+
this.logChange("pull", "add", path, void 0, remoteContent);
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
await this.recordConflict(
|
|
2547
|
+
path,
|
|
2548
|
+
remoteContent ? "Local and Overleaf content differ, but no common checkpoint exists." : "The local path has no matching Overleaf file and no common checkpoint exists."
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
restoreConflicts() {
|
|
2552
|
+
for (const [path, reason] of Object.entries(this.state.conflicts)) {
|
|
2553
|
+
if (this.conflicts.has(path)) {
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
this.conflicts.add(path);
|
|
2557
|
+
this.options.onConflict(path, reason);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
async recordConflict(path, reason) {
|
|
2561
|
+
this.conflicts.add(path);
|
|
2562
|
+
this.state.conflicts[path] = reason;
|
|
2563
|
+
await this.options.stateStore.save(this.state);
|
|
2564
|
+
this.options.onConflict(path, reason);
|
|
2341
2565
|
}
|
|
2342
2566
|
async startupReconcile(currentVersion, localFiles, remoteFiles) {
|
|
2343
2567
|
const state = this.state;
|
|
@@ -2366,6 +2590,11 @@ var SyncEngine = class {
|
|
|
2366
2590
|
}
|
|
2367
2591
|
const candidates = new Set(localChanged);
|
|
2368
2592
|
const knownFiles = /* @__PURE__ */ new Set([...Object.keys(state.files), ...localFiles, ...remoteFiles]);
|
|
2593
|
+
for (const path of [...localFiles, ...remoteFiles]) {
|
|
2594
|
+
if (state.files[path] === void 0) {
|
|
2595
|
+
candidates.add(path);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2369
2598
|
for (const change of remoteDiff.diff) {
|
|
2370
2599
|
const paths = [change.pathname, change.newPathname].filter((path) => Boolean(path));
|
|
2371
2600
|
for (const changedPath of paths.map(normalizePath2)) {
|
|
@@ -2400,6 +2629,9 @@ var SyncEngine = class {
|
|
|
2400
2629
|
}
|
|
2401
2630
|
}
|
|
2402
2631
|
async reconcileAfterReconnect() {
|
|
2632
|
+
if (this.stopped) {
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2403
2635
|
const currentVersion = await this.options.remote.getCurrentVersion();
|
|
2404
2636
|
await this.startupReconcile(
|
|
2405
2637
|
currentVersion,
|
|
@@ -2408,30 +2640,49 @@ var SyncEngine = class {
|
|
|
2408
2640
|
);
|
|
2409
2641
|
this.options.log(`Realtime connection restored at Overleaf version ${currentVersion}.`);
|
|
2410
2642
|
}
|
|
2411
|
-
schedule(path, source) {
|
|
2643
|
+
schedule(path, source, actor) {
|
|
2412
2644
|
const normalized = normalizePath2(path);
|
|
2413
|
-
if (this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2645
|
+
if (this.stopped || this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2414
2646
|
return;
|
|
2415
2647
|
}
|
|
2416
2648
|
const previous = this.eventTimers.get(normalized);
|
|
2417
2649
|
if (previous) {
|
|
2418
|
-
clearTimeout(previous);
|
|
2650
|
+
clearTimeout(previous.timer);
|
|
2419
2651
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2652
|
+
const actors = previous?.actors ?? /* @__PURE__ */ new Set();
|
|
2653
|
+
let unknownRemoteActor = previous?.unknownRemoteActor ?? false;
|
|
2654
|
+
if (source === "remote") {
|
|
2655
|
+
if (actor) {
|
|
2656
|
+
actors.add(actor);
|
|
2657
|
+
} else {
|
|
2658
|
+
unknownRemoteActor = true;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
const pending = {
|
|
2662
|
+
source: previous?.source === "local" || source === "local" ? "local" : "remote",
|
|
2663
|
+
actors,
|
|
2664
|
+
unknownRemoteActor,
|
|
2665
|
+
timer: setTimeout(() => {
|
|
2666
|
+
this.eventTimers.delete(normalized);
|
|
2667
|
+
if (this.stopped) {
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
const remoteActor = pending.unknownRemoteActor ? void 0 : pending.actors.size === 1 ? [...pending.actors][0] : pending.actors.size > 1 ? "multiple Overleaf collaborators" : void 0;
|
|
2671
|
+
this.queue = this.queue.then(() => this.reconcilePath(normalized, pending.source, remoteActor)).catch((error2) => {
|
|
2672
|
+
this.options.log(`Failed to synchronize ${normalized}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
2673
|
+
});
|
|
2674
|
+
}, 250)
|
|
2675
|
+
};
|
|
2676
|
+
this.eventTimers.set(normalized, pending);
|
|
2426
2677
|
}
|
|
2427
2678
|
enqueue(operation) {
|
|
2428
2679
|
const result = this.queue.then(operation);
|
|
2429
2680
|
this.queue = result.then(() => void 0, () => void 0);
|
|
2430
2681
|
return result;
|
|
2431
2682
|
}
|
|
2432
|
-
async reconcilePath(path, source) {
|
|
2683
|
+
async reconcilePath(path, source, actor) {
|
|
2433
2684
|
const normalized = normalizePath2(path);
|
|
2434
|
-
if (this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2685
|
+
if (this.stopped || this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
|
|
2435
2686
|
return;
|
|
2436
2687
|
}
|
|
2437
2688
|
const remoteEntry = this.options.remote.entry(normalized);
|
|
@@ -2441,38 +2692,43 @@ var SyncEngine = class {
|
|
|
2441
2692
|
const localContent = await this.options.local.read(normalized);
|
|
2442
2693
|
const remoteContent = isRemoteFile(remoteEntry) ? await this.options.remote.read(normalized) : void 0;
|
|
2443
2694
|
const baseHash = this.state.files[normalized];
|
|
2695
|
+
const baseContent = readTextBase(this.state, normalized);
|
|
2444
2696
|
const localHash = localContent && sha256(localContent);
|
|
2445
2697
|
const remoteHash = remoteContent && sha256(remoteContent);
|
|
2446
2698
|
if (localHash === remoteHash) {
|
|
2447
|
-
if (
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2699
|
+
if (localHash !== baseHash) {
|
|
2700
|
+
if (localContent) {
|
|
2701
|
+
updateSyncCheckpoint(this.state, normalized, localContent);
|
|
2702
|
+
} else {
|
|
2703
|
+
removeSyncCheckpoint(this.state, normalized);
|
|
2704
|
+
}
|
|
2705
|
+
await this.options.stateStore.save(this.state);
|
|
2451
2706
|
}
|
|
2452
|
-
await this.options.stateStore.save(this.state);
|
|
2453
2707
|
return;
|
|
2454
2708
|
}
|
|
2455
2709
|
const localChanged = localHash !== baseHash;
|
|
2456
2710
|
const remoteChanged = remoteHash !== baseHash;
|
|
2457
2711
|
if (localChanged && remoteChanged) {
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
const merged = mergeText(base, localContent, remoteContent);
|
|
2712
|
+
if (baseContent && localContent && remoteContent) {
|
|
2713
|
+
const merged = mergeText(baseContent, localContent, remoteContent);
|
|
2461
2714
|
if (merged.status === "merged" && merged.content) {
|
|
2462
2715
|
await this.options.local.write(normalized, merged.content);
|
|
2463
2716
|
await this.options.remote.write(normalized, merged.content);
|
|
2464
2717
|
updateSyncCheckpoint(this.state, normalized, merged.content);
|
|
2465
2718
|
await this.options.stateStore.save(this.state);
|
|
2466
|
-
this.logChange("merge", "update", normalized);
|
|
2719
|
+
this.logChange("merge", "update", normalized, baseContent, merged.content, actor);
|
|
2467
2720
|
return;
|
|
2468
2721
|
}
|
|
2469
2722
|
}
|
|
2470
|
-
this.conflicts.add(normalized);
|
|
2471
2723
|
const reason = "Both local and Overleaf content changed since the last synchronized checkpoint.";
|
|
2472
|
-
this.
|
|
2724
|
+
await this.recordConflict(normalized, reason);
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (source === "remote" && localChanged && !remoteChanged) {
|
|
2473
2728
|
return;
|
|
2474
2729
|
}
|
|
2475
2730
|
if (localChanged || !remoteChanged && source === "local") {
|
|
2731
|
+
let pushedContent = localContent;
|
|
2476
2732
|
if (!localContent) {
|
|
2477
2733
|
if (remoteEntry) {
|
|
2478
2734
|
await this.options.remote.remove(normalized);
|
|
@@ -2484,13 +2740,21 @@ var SyncEngine = class {
|
|
|
2484
2740
|
if (sha256(synchronized) !== sha256(localContent)) {
|
|
2485
2741
|
await this.options.local.write(normalized, synchronized);
|
|
2486
2742
|
}
|
|
2743
|
+
pushedContent = synchronized;
|
|
2487
2744
|
updateSyncCheckpoint(this.state, normalized, synchronized);
|
|
2488
2745
|
}
|
|
2489
2746
|
await this.options.stateStore.save(this.state);
|
|
2490
2747
|
if (!localContent && remoteEntry) {
|
|
2491
|
-
this.logChange("push", "delete", normalized);
|
|
2748
|
+
this.logChange("push", "delete", normalized, baseContent ?? remoteContent, void 0, actor);
|
|
2492
2749
|
} else if (localContent) {
|
|
2493
|
-
this.logChange(
|
|
2750
|
+
this.logChange(
|
|
2751
|
+
"push",
|
|
2752
|
+
remoteEntry ? "update" : "add",
|
|
2753
|
+
normalized,
|
|
2754
|
+
baseContent ?? remoteContent,
|
|
2755
|
+
pushedContent,
|
|
2756
|
+
actor
|
|
2757
|
+
);
|
|
2494
2758
|
}
|
|
2495
2759
|
return;
|
|
2496
2760
|
}
|
|
@@ -2505,13 +2769,26 @@ var SyncEngine = class {
|
|
|
2505
2769
|
}
|
|
2506
2770
|
await this.options.stateStore.save(this.state);
|
|
2507
2771
|
if (!remoteContent && localContent) {
|
|
2508
|
-
this.logChange("pull", "delete", normalized);
|
|
2772
|
+
this.logChange("pull", "delete", normalized, baseContent ?? localContent, void 0, actor);
|
|
2509
2773
|
} else if (remoteContent) {
|
|
2510
|
-
this.logChange(
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2774
|
+
this.logChange(
|
|
2775
|
+
"pull",
|
|
2776
|
+
localContent ? "update" : "add",
|
|
2777
|
+
normalized,
|
|
2778
|
+
baseContent ?? localContent,
|
|
2779
|
+
remoteContent,
|
|
2780
|
+
actor
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
logChange(direction, operation, path, before, after, actor) {
|
|
2785
|
+
const summary = describeContentChange(before, after);
|
|
2786
|
+
const source = direction === "push" ? "local filesystem" : direction === "pull" ? actor ?? "Overleaf" : `local filesystem + ${actor ?? "Overleaf"}`;
|
|
2787
|
+
this.options.log([
|
|
2788
|
+
`[${direction}] ${operation} ${JSON.stringify(path)}`,
|
|
2789
|
+
summary,
|
|
2790
|
+
`source=${source}`
|
|
2791
|
+
].filter(Boolean).join("; "));
|
|
2515
2792
|
}
|
|
2516
2793
|
remoteFilePaths() {
|
|
2517
2794
|
return this.options.remote.listEntries().filter((entry) => isRemoteFile(entry) && !this.shouldIgnore(entry.path)).map((entry) => entry.path);
|
|
@@ -2520,39 +2797,325 @@ var SyncEngine = class {
|
|
|
2520
2797
|
return this.options.ignore(path) || path.split("/").some((part) => part.startsWith("."));
|
|
2521
2798
|
}
|
|
2522
2799
|
};
|
|
2800
|
+
var HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
2801
|
+
var encoder = new TextEncoder();
|
|
2802
|
+
function json(value) {
|
|
2803
|
+
return `${JSON.stringify(value, null, 2)}
|
|
2804
|
+
`;
|
|
2805
|
+
}
|
|
2806
|
+
function pathKey(path) {
|
|
2807
|
+
return sha256(encoder.encode(path));
|
|
2808
|
+
}
|
|
2809
|
+
async function readJson(path) {
|
|
2810
|
+
return JSON.parse(await readFile4(path, "utf8"));
|
|
2811
|
+
}
|
|
2812
|
+
async function listFiles(directory) {
|
|
2813
|
+
const result = [];
|
|
2814
|
+
const visit = async (current) => {
|
|
2815
|
+
let entries;
|
|
2816
|
+
try {
|
|
2817
|
+
entries = await readdir2(current, { withFileTypes: true });
|
|
2818
|
+
} catch (error2) {
|
|
2819
|
+
if (error2.code === "ENOENT") {
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
throw error2;
|
|
2823
|
+
}
|
|
2824
|
+
for (const entry of entries) {
|
|
2825
|
+
const path = join2(current, entry.name);
|
|
2826
|
+
if (entry.isDirectory()) {
|
|
2827
|
+
await visit(path);
|
|
2828
|
+
} else if (entry.isFile()) {
|
|
2829
|
+
result.push(path);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
await visit(directory);
|
|
2834
|
+
return result;
|
|
2835
|
+
}
|
|
2836
|
+
async function atomicWrite(path, content) {
|
|
2837
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
2838
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
2839
|
+
try {
|
|
2840
|
+
await writeFile3(temporary, content);
|
|
2841
|
+
await rename(temporary, path);
|
|
2842
|
+
} finally {
|
|
2843
|
+
await rm2(temporary, { force: true });
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
function parseFormat(value, projectUri) {
|
|
2847
|
+
if (typeof value !== "object" || value === null) {
|
|
2848
|
+
return false;
|
|
2849
|
+
}
|
|
2850
|
+
const format = value;
|
|
2851
|
+
return format.schemaVersion === SYNC_STATE_SCHEMA_VERSION && format.projectUri === projectUri;
|
|
2852
|
+
}
|
|
2853
|
+
function parseCursor(value) {
|
|
2854
|
+
if (typeof value !== "object" || value === null) {
|
|
2855
|
+
return false;
|
|
2856
|
+
}
|
|
2857
|
+
const cursor = value;
|
|
2858
|
+
return Number.isInteger(cursor.remoteVersion) && cursor.remoteVersion >= 0 && typeof cursor.initialized === "boolean";
|
|
2859
|
+
}
|
|
2860
|
+
function parsePathRecord(value) {
|
|
2861
|
+
if (typeof value !== "object" || value === null) {
|
|
2862
|
+
return false;
|
|
2863
|
+
}
|
|
2864
|
+
const record = value;
|
|
2865
|
+
if (typeof record.path !== "string" || !record.path.startsWith("/")) {
|
|
2866
|
+
return false;
|
|
2867
|
+
}
|
|
2868
|
+
if (record.hash !== void 0 && (typeof record.hash !== "string" || !HASH_PATTERN.test(record.hash))) {
|
|
2869
|
+
return false;
|
|
2870
|
+
}
|
|
2871
|
+
if (record.baseHash !== void 0 && (typeof record.baseHash !== "string" || !HASH_PATTERN.test(record.baseHash) || record.hash !== record.baseHash)) {
|
|
2872
|
+
return false;
|
|
2873
|
+
}
|
|
2874
|
+
if (record.conflict !== void 0 && typeof record.conflict !== "string") {
|
|
2875
|
+
return false;
|
|
2876
|
+
}
|
|
2877
|
+
return record.hash !== void 0 || record.conflict !== void 0;
|
|
2878
|
+
}
|
|
2879
|
+
function createRecord(state, path) {
|
|
2880
|
+
const hash = state.files[path];
|
|
2881
|
+
const textBase = state.textBases[path];
|
|
2882
|
+
const conflict = state.conflicts[path];
|
|
2883
|
+
if (hash === void 0 && conflict === void 0) {
|
|
2884
|
+
return void 0;
|
|
2885
|
+
}
|
|
2886
|
+
return {
|
|
2887
|
+
path,
|
|
2888
|
+
...hash === void 0 ? {} : { hash },
|
|
2889
|
+
...textBase === void 0 ? {} : { baseHash: hash },
|
|
2890
|
+
...conflict === void 0 ? {} : { conflict }
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2523
2893
|
var FileSyncStateStore = class {
|
|
2524
2894
|
constructor(directory, projectUri) {
|
|
2525
2895
|
this.projectUri = projectUri;
|
|
2526
|
-
|
|
2896
|
+
const overleafDirectory = resolve4(directory, ".overleaf");
|
|
2897
|
+
this.legacyPath = join2(overleafDirectory, "sync-state.json");
|
|
2898
|
+
this.syncDirectory = join2(overleafDirectory, "sync");
|
|
2899
|
+
this.formatPath = join2(this.syncDirectory, "format.json");
|
|
2900
|
+
this.cursorPath = join2(this.syncDirectory, "cursor.json");
|
|
2901
|
+
this.pathsDirectory = join2(this.syncDirectory, "paths");
|
|
2902
|
+
this.basesDirectory = join2(this.syncDirectory, "bases");
|
|
2527
2903
|
}
|
|
2528
2904
|
projectUri;
|
|
2529
|
-
|
|
2530
|
-
|
|
2905
|
+
legacyPath;
|
|
2906
|
+
syncDirectory;
|
|
2907
|
+
formatPath;
|
|
2908
|
+
cursorPath;
|
|
2909
|
+
pathsDirectory;
|
|
2910
|
+
basesDirectory;
|
|
2911
|
+
persistedRecords = /* @__PURE__ */ new Map();
|
|
2912
|
+
persistedCursor;
|
|
2913
|
+
invalidRecordPaths = /* @__PURE__ */ new Set();
|
|
2914
|
+
initialized = false;
|
|
2531
2915
|
async load() {
|
|
2916
|
+
this.invalidRecordPaths.clear();
|
|
2532
2917
|
try {
|
|
2533
|
-
const
|
|
2534
|
-
const
|
|
2535
|
-
if (
|
|
2536
|
-
|
|
2537
|
-
`;
|
|
2918
|
+
const format = await readJson(this.formatPath);
|
|
2919
|
+
const cursor = await readJson(this.cursorPath);
|
|
2920
|
+
if (!parseFormat(format, this.projectUri) || !parseCursor(cursor)) {
|
|
2921
|
+
return void 0;
|
|
2538
2922
|
}
|
|
2923
|
+
const state = {
|
|
2924
|
+
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
2925
|
+
projectUri: this.projectUri,
|
|
2926
|
+
remoteVersion: cursor.remoteVersion,
|
|
2927
|
+
initialized: cursor.initialized,
|
|
2928
|
+
files: {},
|
|
2929
|
+
textBases: {},
|
|
2930
|
+
conflicts: {}
|
|
2931
|
+
};
|
|
2932
|
+
const records = /* @__PURE__ */ new Map();
|
|
2933
|
+
const referencedBases = /* @__PURE__ */ new Set();
|
|
2934
|
+
let stateIncomplete = false;
|
|
2935
|
+
for (const recordPath of await listFiles(this.pathsDirectory)) {
|
|
2936
|
+
const record = await this.loadRecord(recordPath);
|
|
2937
|
+
if (!record) {
|
|
2938
|
+
if (recordPath.endsWith(".json")) {
|
|
2939
|
+
this.invalidRecordPaths.add(recordPath);
|
|
2940
|
+
stateIncomplete = true;
|
|
2941
|
+
} else {
|
|
2942
|
+
await rm2(recordPath, { force: true });
|
|
2943
|
+
}
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
if (record.hash !== void 0) {
|
|
2947
|
+
state.files[record.path] = record.hash;
|
|
2948
|
+
}
|
|
2949
|
+
if (record.baseHash !== void 0) {
|
|
2950
|
+
let textBase;
|
|
2951
|
+
try {
|
|
2952
|
+
textBase = await readFile4(this.basePath(record.baseHash), "utf8");
|
|
2953
|
+
} catch (error2) {
|
|
2954
|
+
if (error2.code === "ENOENT") {
|
|
2955
|
+
this.invalidRecordPaths.add(recordPath);
|
|
2956
|
+
stateIncomplete = true;
|
|
2957
|
+
delete state.files[record.path];
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
throw error2;
|
|
2961
|
+
}
|
|
2962
|
+
if (sha256(encoder.encode(textBase)) !== record.baseHash) {
|
|
2963
|
+
this.invalidRecordPaths.add(recordPath);
|
|
2964
|
+
stateIncomplete = true;
|
|
2965
|
+
delete state.files[record.path];
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
state.textBases[record.path] = textBase;
|
|
2969
|
+
referencedBases.add(record.baseHash);
|
|
2970
|
+
}
|
|
2971
|
+
if (record.conflict !== void 0) {
|
|
2972
|
+
state.conflicts[record.path] = record.conflict;
|
|
2973
|
+
}
|
|
2974
|
+
records.set(record.path, json(record));
|
|
2975
|
+
}
|
|
2976
|
+
if (stateIncomplete) {
|
|
2977
|
+
state.initialized = false;
|
|
2978
|
+
}
|
|
2979
|
+
await this.removeOrphanBases(referencedBases);
|
|
2980
|
+
this.persistedRecords = records;
|
|
2981
|
+
this.persistedCursor = json(cursor);
|
|
2982
|
+
this.initialized = true;
|
|
2539
2983
|
return state;
|
|
2540
2984
|
} catch (error2) {
|
|
2541
|
-
if (error2.code === "ENOENT") {
|
|
2985
|
+
if (error2.code === "ENOENT" || error2 instanceof SyntaxError) {
|
|
2542
2986
|
return void 0;
|
|
2543
2987
|
}
|
|
2544
2988
|
throw error2;
|
|
2545
2989
|
}
|
|
2546
2990
|
}
|
|
2547
2991
|
async save(state) {
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2992
|
+
if (state.projectUri !== this.projectUri || state.schemaVersion !== SYNC_STATE_SCHEMA_VERSION) {
|
|
2993
|
+
throw new Error("Cannot persist synchronization state for a different project or schema.");
|
|
2994
|
+
}
|
|
2995
|
+
if (!this.initialized) {
|
|
2996
|
+
await this.initializeStorage();
|
|
2997
|
+
}
|
|
2998
|
+
const paths = /* @__PURE__ */ new Set([
|
|
2999
|
+
...this.persistedRecords.keys(),
|
|
3000
|
+
...Object.keys(state.files),
|
|
3001
|
+
...Object.keys(state.conflicts)
|
|
3002
|
+
]);
|
|
3003
|
+
const obsoleteBases = /* @__PURE__ */ new Set();
|
|
3004
|
+
for (const path of [...paths].sort()) {
|
|
3005
|
+
const record = createRecord(state, path);
|
|
3006
|
+
const serialized = record && json(record);
|
|
3007
|
+
const previous = this.persistedRecords.get(path);
|
|
3008
|
+
if (serialized === previous) {
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
if (previous) {
|
|
3012
|
+
const previousRecord = JSON.parse(previous);
|
|
3013
|
+
if (previousRecord.baseHash) {
|
|
3014
|
+
obsoleteBases.add(previousRecord.baseHash);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
if (!record || !serialized) {
|
|
3018
|
+
await rm2(this.recordPath(path), { force: true });
|
|
3019
|
+
this.persistedRecords.delete(path);
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
if (record.baseHash) {
|
|
3023
|
+
const textBase = state.textBases[path];
|
|
3024
|
+
if (textBase === void 0 || sha256(encoder.encode(textBase)) !== record.baseHash) {
|
|
3025
|
+
throw new Error(`Text checkpoint does not match its content hash: ${path}`);
|
|
3026
|
+
}
|
|
3027
|
+
await this.writeBase(record.baseHash, textBase);
|
|
3028
|
+
}
|
|
3029
|
+
const destination = this.recordPath(path);
|
|
3030
|
+
await atomicWrite(destination, serialized);
|
|
3031
|
+
this.invalidRecordPaths.delete(destination);
|
|
3032
|
+
this.persistedRecords.set(path, serialized);
|
|
3033
|
+
}
|
|
3034
|
+
const cursor = json({ remoteVersion: state.remoteVersion, initialized: state.initialized });
|
|
3035
|
+
if (cursor !== this.persistedCursor) {
|
|
3036
|
+
await atomicWrite(this.cursorPath, cursor);
|
|
3037
|
+
this.persistedCursor = cursor;
|
|
3038
|
+
}
|
|
3039
|
+
if (state.initialized) {
|
|
3040
|
+
await Promise.all([...this.invalidRecordPaths].map((path) => rm2(path, { force: true })));
|
|
3041
|
+
this.invalidRecordPaths.clear();
|
|
3042
|
+
}
|
|
3043
|
+
await rm2(this.legacyPath, { force: true });
|
|
3044
|
+
await this.removeObsoleteBases(state, obsoleteBases);
|
|
3045
|
+
}
|
|
3046
|
+
async initializeStorage() {
|
|
3047
|
+
await rm2(this.syncDirectory, { recursive: true, force: true });
|
|
3048
|
+
await mkdir3(this.pathsDirectory, { recursive: true });
|
|
3049
|
+
await mkdir3(this.basesDirectory, { recursive: true });
|
|
3050
|
+
await atomicWrite(this.formatPath, json({
|
|
3051
|
+
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
3052
|
+
projectUri: this.projectUri
|
|
3053
|
+
}));
|
|
3054
|
+
this.persistedRecords.clear();
|
|
3055
|
+
this.persistedCursor = void 0;
|
|
3056
|
+
this.invalidRecordPaths.clear();
|
|
3057
|
+
this.initialized = true;
|
|
3058
|
+
}
|
|
3059
|
+
recordPath(path) {
|
|
3060
|
+
const key = pathKey(path);
|
|
3061
|
+
return join2(this.pathsDirectory, key.slice(0, 2), `${key}.json`);
|
|
3062
|
+
}
|
|
3063
|
+
async loadRecord(recordPath) {
|
|
3064
|
+
if (!recordPath.endsWith(".json")) {
|
|
3065
|
+
return void 0;
|
|
3066
|
+
}
|
|
3067
|
+
try {
|
|
3068
|
+
const record = await readJson(recordPath);
|
|
3069
|
+
if (!parsePathRecord(record) || recordPath !== this.recordPath(record.path)) {
|
|
3070
|
+
return void 0;
|
|
3071
|
+
}
|
|
3072
|
+
return record;
|
|
3073
|
+
} catch (error2) {
|
|
3074
|
+
if (error2.code === "ENOENT" || error2 instanceof SyntaxError) {
|
|
3075
|
+
return void 0;
|
|
3076
|
+
}
|
|
3077
|
+
throw error2;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
basePath(hash) {
|
|
3081
|
+
return join2(this.basesDirectory, hash.slice(0, 2), `${hash}.txt`);
|
|
3082
|
+
}
|
|
3083
|
+
async writeBase(hash, content) {
|
|
3084
|
+
const path = this.basePath(hash);
|
|
3085
|
+
try {
|
|
3086
|
+
if (sha256(encoder.encode(await readFile4(path, "utf8"))) === hash) {
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
} catch (error2) {
|
|
3090
|
+
if (error2.code !== "ENOENT") {
|
|
3091
|
+
throw error2;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
await atomicWrite(path, content);
|
|
3095
|
+
}
|
|
3096
|
+
async removeObsoleteBases(state, candidates) {
|
|
3097
|
+
if (candidates.size === 0) {
|
|
2551
3098
|
return;
|
|
2552
3099
|
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
3100
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
3101
|
+
for (const [path, textBase] of Object.entries(state.textBases)) {
|
|
3102
|
+
if (textBase !== void 0 && state.files[path] !== void 0) {
|
|
3103
|
+
referenced.add(state.files[path]);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
for (const hash of candidates) {
|
|
3107
|
+
if (!referenced.has(hash)) {
|
|
3108
|
+
await rm2(this.basePath(hash), { force: true });
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
async removeOrphanBases(referencedHashes) {
|
|
3113
|
+
const referencedPaths = new Set([...referencedHashes].map((hash) => this.basePath(hash)));
|
|
3114
|
+
for (const path of await listFiles(this.basesDirectory)) {
|
|
3115
|
+
if (!referencedPaths.has(path)) {
|
|
3116
|
+
await rm2(path, { force: true });
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
2556
3119
|
}
|
|
2557
3120
|
};
|
|
2558
3121
|
async function startServe(directory = process.cwd(), options = {}) {
|
|
@@ -2618,4 +3181,4 @@ export {
|
|
|
2618
3181
|
RemoteProject,
|
|
2619
3182
|
startServe
|
|
2620
3183
|
};
|
|
2621
|
-
//# sourceMappingURL=chunk-
|
|
3184
|
+
//# sourceMappingURL=chunk-OZ4FLSVP.js.map
|