mainflow 0.41.0 → 0.42.0
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/cli.js +2 -2
- package/dist/hooks.js +1 -1
- package/dist/{launch-CMeKJsNd.js → launch-DosYhbwc.js} +1 -1
- package/dist/launch.js +1 -1
- package/dist/native/mainflow-swap-darwin-arm64 +0 -0
- package/dist/native/mainflow-swap-darwin-x64 +0 -0
- package/dist/{operation-DlR1P_Pt.js → operation-2wx2ex6x.js} +229 -20
- package/dist/{ui-9uuI4Jif.js → ui-D1dg19Jw.js} +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { $ as stampMs, G as parseUntil, K as refExample, M as UNTIL_FORMS, W as parseRef, n as CLOSED_STATUS } from "./dist-zSabglPq.js";
|
|
3
3
|
import { At as WriteError, Ct as TASK_FILE, D as loadSessions, Et as findHubRoot, G as applyOperation, K as checkHubName, O as readSession, St as MAINLINE_FILE, X as sectionText, Y as loadHub, at as nowMinute, b as duplicateClaimLines, ct as nodeStorage, d as migrateHub, et as memberOfGroup, f as retroReport, ft as MAX_FILE_BYTES, h as RETRY_RULES, kt as DATA_DIR, m as receiverFromEnv, nt as remoteMembers, p as retroReportFromReceiver, q as HubError, rt as unavailableHubs, u as groupPlan, v as initHub, wt as requireSlug, x as sessionBriefing, y as untilState } from "./host-BAMZmRy8.js";
|
|
4
4
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
5
|
-
import { C as ServerError, D as syncSettings, E as hubName, O as SETTINGS_FILE, S as ServerApi, T as earlierToken, _ as readDaemon, a as remoteMode, b as readRemote, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, j as syncPaths, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as ENV, x as oversizedFiles } from "./operation-
|
|
5
|
+
import { C as ServerError, D as syncSettings, E as hubName, O as SETTINGS_FILE, S as ServerApi, T as earlierToken, _ as readDaemon, a as remoteMode, b as readRemote, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, j as syncPaths, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as ENV, x as oversizedFiles } from "./operation-2wx2ex6x.js";
|
|
6
6
|
import { COMMAND_SPECS, OPTIONS } from "./facts.js";
|
|
7
7
|
import { hookFallback, parsePayload, runHook } from "./hooks.js";
|
|
8
|
-
import { f as CLI_VERSION, i as launchUi, l as startUiInBackground, r as exitCodeFor, u as watchHubGone } from "./launch-
|
|
8
|
+
import { f as CLI_VERSION, i as launchUi, l as startUiInBackground, r as exitCodeFor, u as watchHubGone } from "./launch-DosYhbwc.js";
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
11
11
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
package/dist/hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { P as configSchema, W as parseRef, Y as taskSchema, rt as describeRequest } from "./dist-zSabglPq.js";
|
|
2
2
|
import { A as grantScope, D as loadSessions, Dt as worktreesOf, Et as findHubRoot, F as mayDecorate, M as decorateReply, N as endsWithMarker, O as readSession, P as headerLine, S as turnReminder, Tt as currentBranch, Y as loadHub, bt as formatMismatch, ct as nodeStorage, i as hostName, it as taskFile, j as matchGrants, kt as DATA_DIR, ot as readFrontMatter, q as HubError, st as readYaml, x as sessionBriefing, xt as hubFormatVersion } from "./host-BAMZmRy8.js";
|
|
3
|
-
import { n as hubWriter, r as writtenLines } from "./operation-
|
|
3
|
+
import { n as hubWriter, r as writtenLines } from "./operation-2wx2ex6x.js";
|
|
4
4
|
import { SCENARIO_LINES, SCENARIO_SKIP_KINDS } from "./facts.js";
|
|
5
5
|
import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
|
|
6
6
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
@@ -276,7 +276,7 @@ async function launchUi(root, hub, configured, options) {
|
|
|
276
276
|
}
|
|
277
277
|
let server;
|
|
278
278
|
if (!reuse) {
|
|
279
|
-
const { serveUi } = await import("./ui-
|
|
279
|
+
const { serveUi } = await import("./ui-D1dg19Jw.js");
|
|
280
280
|
server = await serveUi(root, choice.port);
|
|
281
281
|
writeUiRecord(root, {
|
|
282
282
|
port: choice.port,
|
package/dist/launch.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as networkProbe, c as realRoot, d as withUiLock, i as launchUi, l as startUiInBackground, n as choosePort, o as portCandidates, r as exitCodeFor, s as readUiRecord, t as PORT_SPAN, u as watchHubGone } from "./launch-
|
|
1
|
+
import { a as networkProbe, c as realRoot, d as withUiLock, i as launchUi, l as startUiInBackground, n as choosePort, o as portCandidates, r as exitCodeFor, s as readUiRecord, t as PORT_SPAN, u as watchHubGone } from "./launch-DosYhbwc.js";
|
|
2
2
|
export { PORT_SPAN, choosePort, exitCodeFor, launchUi, networkProbe, portCandidates, readUiRecord, realRoot, startUiInBackground, watchHubGone, withUiLock };
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { At as WriteError, B as renumberRequest, G as applyOperation, H as renumberPath, Mt as splitFrontMatter, Ot as ConflictError, U as renumberText, V as renumberFiles, W as scopeOfPath, _t as readsAsText, at as nowMinute, ct as nodeStorage, dt as MAX_BLOB_BYTES, ft as MAX_FILE_BYTES, gt as exceedsFileLimit, ht as comparePaths, jt as contentHash, kt as DATA_DIR, lt as ACCESS_HEADERS, mt as SOCKET_MAX_AGE_CLOSE, pt as ROUTES, ut as BLOB_HASH, vt as requestTime, yt as textBytes, z as applyOffline } from "./host-BAMZmRy8.js";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, createReadStream, createWriteStream, existsSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
3
|
+
import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, createReadStream, createWriteStream, existsSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -900,7 +900,8 @@ const requestIdOf = (entry) => entry.kind === "blob" && entry.retry ? `${entry.i
|
|
|
900
900
|
const waiting = (entry) => entry.refused === void 0;
|
|
901
901
|
/** A new entry's id. */
|
|
902
902
|
const entryId = () => randomBytes(8).toString("hex");
|
|
903
|
-
|
|
903
|
+
/** The files an entry writes; none for a directory's removal. */
|
|
904
|
+
const entryPaths = (entry) => entry.kind === "op" ? Object.keys(entry.written) : entry.kind === "rmdir" ? [] : [entry.path];
|
|
904
905
|
/** A local minute without an offset, the `now` of protocol 1 */
|
|
905
906
|
const LOCAL_MINUTE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/;
|
|
906
907
|
/** Protocols whose write requests read the same as the current one's: protocol 3 changed only how a snapshot is taken. */
|
|
@@ -954,6 +955,11 @@ function writeJson(file, value) {
|
|
|
954
955
|
renameSync(temp, file);
|
|
955
956
|
}
|
|
956
957
|
const readKnown = (root) => readJson(syncPaths(root).known, {}).files ?? {};
|
|
958
|
+
/** The directories known.json names; none from a client before it kept them. */
|
|
959
|
+
const readKnownDirs = (root) => {
|
|
960
|
+
const dirs = readJson(syncPaths(root).known, {}).dirs;
|
|
961
|
+
return Array.isArray(dirs) ? dirs.filter((dir) => typeof dir === "string") : [];
|
|
962
|
+
};
|
|
957
963
|
/** What known.json writes before a binary file's sha256, apart from a text's content hash. */
|
|
958
964
|
const BLOB_PREFIX = "blob:";
|
|
959
965
|
/** The text of the version known.json names by `hash`; undefined when it was not kept, as by a sync client before known/. */
|
|
@@ -970,8 +976,9 @@ function readKnownText(root, hash) {
|
|
|
970
976
|
* there yet; the texts of versions it no longer names are removed. A version whose text the client does not have keeps
|
|
971
977
|
* the text known/ holds, if any. The new texts go first, then known.json, then the removals, so a client killed at any
|
|
972
978
|
* step leaves a known.json whose every text is in known/. A binary file is given as its sha256 with BLOB_PREFIX, and no text.
|
|
979
|
+
* `dirs` are the directories it names.
|
|
973
980
|
*/
|
|
974
|
-
function writeKnown(root, files) {
|
|
981
|
+
function writeKnown(root, files, dirs = []) {
|
|
975
982
|
const dir = syncPaths(root).knownTexts;
|
|
976
983
|
mkdirSync(dir, { recursive: true });
|
|
977
984
|
const named = new Set(Object.values(files).map((file) => file.hash));
|
|
@@ -982,7 +989,10 @@ function writeKnown(root, files) {
|
|
|
982
989
|
renameSync(join(dir, `${hash}.tmp`), join(dir, hash));
|
|
983
990
|
held.add(hash);
|
|
984
991
|
}
|
|
985
|
-
const known = {
|
|
992
|
+
const known = {
|
|
993
|
+
files: Object.fromEntries(Object.entries(files).map(([path, file]) => [path, file.hash])),
|
|
994
|
+
...dirs.length > 0 ? { dirs } : {}
|
|
995
|
+
};
|
|
986
996
|
writeJson(syncPaths(root).known, known);
|
|
987
997
|
for (const name of held) if (!named.has(name)) rmSync(join(dir, name), { force: true });
|
|
988
998
|
}
|
|
@@ -1510,7 +1520,7 @@ function heldBase(entry) {
|
|
|
1510
1520
|
*/
|
|
1511
1521
|
function textAt(entry, path) {
|
|
1512
1522
|
if (entry.kind === "op") return entry.written[path];
|
|
1513
|
-
if (entry.path !== path || entry.kind === "blob") return void 0;
|
|
1523
|
+
if (entry.path !== path || entry.kind === "blob" || entry.kind === "rmdir") return void 0;
|
|
1514
1524
|
if (entry.kind === "remove") return null;
|
|
1515
1525
|
return entry.text;
|
|
1516
1526
|
}
|
|
@@ -1670,6 +1680,15 @@ var SyncClient = class extends EventEmitter {
|
|
|
1670
1680
|
unheld = /* @__PURE__ */ new Set();
|
|
1671
1681
|
/** The hub's seq of each path's version in `cloud`: a version from the server with a seq no higher is not newer */
|
|
1672
1682
|
fileSeq = /* @__PURE__ */ new Map();
|
|
1683
|
+
/** The hub's seq of each directory as the server last named it, held or removed, as fileSeq is of a file */
|
|
1684
|
+
dirSeq = /* @__PURE__ */ new Map();
|
|
1685
|
+
/**
|
|
1686
|
+
* The directories the server held that the client had here, as known.json keeps them: one gone here is sent as removed
|
|
1687
|
+
* (queueRemovedDirs), and one a snapshot names is made here only when it is not among them
|
|
1688
|
+
*/
|
|
1689
|
+
syncedDirs = /* @__PURE__ */ new Set();
|
|
1690
|
+
/** The check for directories gone here that is to run a moment from now (checkDirs) */
|
|
1691
|
+
dirCheck = null;
|
|
1673
1692
|
/** The hub's seq up to which the client has taken in every change: the last snapshot and the broadcasts after it */
|
|
1674
1693
|
hubSeq = -1;
|
|
1675
1694
|
/** The seqs of the broadcasts on the current connection above hubSeq with a change between that never arrived */
|
|
@@ -1777,6 +1796,13 @@ var SyncClient = class extends EventEmitter {
|
|
|
1777
1796
|
return this.scope.synced(path);
|
|
1778
1797
|
}
|
|
1779
1798
|
/**
|
|
1799
|
+
* Whether the directory at `path` is synced: one that may hold synced files and that the hub's .gitignore files leave
|
|
1800
|
+
* in, apart from local/ itself, which every machine has of its own (src/paths.ts).
|
|
1801
|
+
*/
|
|
1802
|
+
syncsDir(path) {
|
|
1803
|
+
return path !== "" && path !== "local" && this.scope.walks(path);
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1780
1806
|
* Whether the hub's .gitignore files changed since the last snapshot was asked for: a file they let in since may be on
|
|
1781
1807
|
* the server although the client took nothing of it in.
|
|
1782
1808
|
*/
|
|
@@ -1859,6 +1885,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1859
1885
|
async restoreKnown() {
|
|
1860
1886
|
this.copied = !existsSync(this.paths.known);
|
|
1861
1887
|
this.blobsCompared = !this.copied && !existsSync(this.paths.copyBlobs);
|
|
1888
|
+
for (const dir of readKnownDirs(this.root)) this.syncedDirs.add(dir);
|
|
1862
1889
|
for (const [path, hash] of Object.entries(readKnown(this.root))) {
|
|
1863
1890
|
if (hash.startsWith("blob:")) {
|
|
1864
1891
|
const disk = {
|
|
@@ -2113,7 +2140,9 @@ var SyncClient = class extends EventEmitter {
|
|
|
2113
2140
|
this.track(this.resync());
|
|
2114
2141
|
return;
|
|
2115
2142
|
}
|
|
2116
|
-
for (const event of message.files) this.
|
|
2143
|
+
for (const event of message.files) if (event.kind === "dir") this.receiveDir(event.path, seq, true);
|
|
2144
|
+
else if (event.kind === "dirRemoved") this.receiveDirRemoved(event.path, seq);
|
|
2145
|
+
else this.receive(event, seq);
|
|
2117
2146
|
if (this.ahead.size > 0 && this.synced && this.resyncing === null) {
|
|
2118
2147
|
this.log(`missed the hub's changes ${this.hubSeq + 1} to ${Math.min(...this.ahead) - 1}; reloading the snapshot`);
|
|
2119
2148
|
this.track(this.resync());
|
|
@@ -2135,10 +2164,6 @@ var SyncClient = class extends EventEmitter {
|
|
|
2135
2164
|
* as a listed text does in a snapshot.
|
|
2136
2165
|
*/
|
|
2137
2166
|
receive(event, seq) {
|
|
2138
|
-
if (event.kind === "dir") {
|
|
2139
|
-
if ((event.path.split("/")[0] !== "local" || event.path === "local/sessions") && !this.scope.ignored(event.path, true)) this.track(mkdir(join(this.paths.data, event.path), { recursive: true }));
|
|
2140
|
-
return;
|
|
2141
|
-
}
|
|
2142
2167
|
if (!this.syncs(event.path)) return;
|
|
2143
2168
|
const file = event.kind === "file" ? event.text : event.kind === "blob" ? {
|
|
2144
2169
|
kind: "blob",
|
|
@@ -2180,6 +2205,58 @@ var SyncClient = class extends EventEmitter {
|
|
|
2180
2205
|
const mainline = MAINLINE_FILE.exec(event.path)?.[1];
|
|
2181
2206
|
if (mainline !== void 0 && this.remote.size > 0) this.fetchOpened(mainline);
|
|
2182
2207
|
}
|
|
2208
|
+
/**
|
|
2209
|
+
* A directory the server holds as of the hub's `seq`, from a broadcast (`live`) or a snapshot. One the client had here
|
|
2210
|
+
* and that is gone now, named by a snapshot, was removed here: it is not made again, and its removal is queued
|
|
2211
|
+
* (queueRemovedDirs). Any other is made here and known from then on; a broadcast's always is, an operation having just
|
|
2212
|
+
* made it. A directory the client does not sync, and a version no newer than the one the client holds, as a snapshot's
|
|
2213
|
+
* after the broadcast of its removal, are left alone.
|
|
2214
|
+
*/
|
|
2215
|
+
receiveDir(path, seq, live) {
|
|
2216
|
+
if (!this.syncsDir(path) || seq <= (this.dirSeq.get(path) ?? -1)) return;
|
|
2217
|
+
this.dirSeq.set(path, seq);
|
|
2218
|
+
const dir = join(this.paths.data, path);
|
|
2219
|
+
if (!live && this.syncedDirs.has(path) && !existsSync(dir)) {
|
|
2220
|
+
this.trace(`dir ${path} seq ${seq}: removed here`);
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
this.knowDir(path);
|
|
2224
|
+
this.track(mkdir(dir, { recursive: true }));
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* The server removed the directory at `path` as of the hub's `seq`, as a broadcast tells, or a snapshot that no longer
|
|
2228
|
+
* names it: removed here too when nothing is in it, under the write lock, so no mainflow write puts a file in it
|
|
2229
|
+
* meanwhile. One that holds anything, as a file written here not sent yet, stays; the file's write makes it on the
|
|
2230
|
+
* server again.
|
|
2231
|
+
*/
|
|
2232
|
+
receiveDirRemoved(path, seq) {
|
|
2233
|
+
if (!this.syncsDir(path) || seq <= (this.dirSeq.get(path) ?? -1)) return;
|
|
2234
|
+
this.dirSeq.set(path, seq);
|
|
2235
|
+
this.forgetDir(path);
|
|
2236
|
+
this.trace(`dir ${path} removed seq ${seq}`);
|
|
2237
|
+
this.track(this.whenFree(() => this.locked(() => this.removeEmptyDir(path))).catch((error) => this.log(`${path}/: ${error.message}`)));
|
|
2238
|
+
}
|
|
2239
|
+
/** Remove the directory at `path` when nothing is in it; one gone already, or holding anything, stays as it is. True when it went. */
|
|
2240
|
+
removeEmptyDir(path) {
|
|
2241
|
+
try {
|
|
2242
|
+
rmdirSync(join(this.paths.data, path));
|
|
2243
|
+
return true;
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
const code = error.code;
|
|
2246
|
+
if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "ENOTDIR") throw error;
|
|
2247
|
+
return false;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
/** The server holds the directory at `path`, and the client has it here: known from now on. */
|
|
2251
|
+
knowDir(path) {
|
|
2252
|
+
if (this.syncedDirs.has(path)) return;
|
|
2253
|
+
this.syncedDirs.add(path);
|
|
2254
|
+
this.saveKnown();
|
|
2255
|
+
}
|
|
2256
|
+
/** The server no longer holds the directory at `path`. */
|
|
2257
|
+
forgetDir(path) {
|
|
2258
|
+
if (this.syncedDirs.delete(path)) this.saveKnown();
|
|
2259
|
+
}
|
|
2183
2260
|
/** Whether the client holds `path` as of the hub's `seq` or later. */
|
|
2184
2261
|
reached(path, seq) {
|
|
2185
2262
|
return this.hubSeq >= seq || (this.fileSeq.get(path) ?? -1) >= seq;
|
|
@@ -2247,6 +2324,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2247
2324
|
this.log(`the sync server's snapshot stays without the hub's changes ${missing}; taking it as complete`);
|
|
2248
2325
|
this.ahead.clear();
|
|
2249
2326
|
for (const [path, seq] of this.fileSeq) if (seq > this.hubSeq) this.fileSeq.delete(path);
|
|
2327
|
+
for (const [path, seq] of this.dirSeq) if (seq > this.hubSeq) this.dirSeq.delete(path);
|
|
2250
2328
|
stalled = 0;
|
|
2251
2329
|
this.resyncAgain = true;
|
|
2252
2330
|
continue;
|
|
@@ -2362,6 +2440,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2362
2440
|
if (seq < before) {
|
|
2363
2441
|
this.log(`the sync server's seq went back from ${before} to ${seq}; taking its snapshot as it is`);
|
|
2364
2442
|
this.fileSeq.clear();
|
|
2443
|
+
this.dirSeq.clear();
|
|
2365
2444
|
this.hubSeq = -1;
|
|
2366
2445
|
this.ahead.clear();
|
|
2367
2446
|
}
|
|
@@ -2372,8 +2451,16 @@ var SyncClient = class extends EventEmitter {
|
|
|
2372
2451
|
this.snapshotAnswers.clear();
|
|
2373
2452
|
const fromCopy = this.copied && !this.synced;
|
|
2374
2453
|
const blobsFromCopy = withBlobs && !this.blobsCompared;
|
|
2454
|
+
/** The directories the snapshot names: every one the server holds */
|
|
2455
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
2375
2456
|
for (const part of parts) for (const event of part.files) {
|
|
2376
|
-
if (event.kind
|
|
2457
|
+
if (event.kind === "dir") {
|
|
2458
|
+
named.add(event.path);
|
|
2459
|
+
dirs.add(event.path);
|
|
2460
|
+
this.receiveDir(event.path, part.seq, false);
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
if (!this.syncs(event.path)) continue;
|
|
2377
2464
|
if (event.kind === "listed") {
|
|
2378
2465
|
named.add(event.path);
|
|
2379
2466
|
if (this.mayStay(event.path, held)) this.keepRemote(event.path, event.hash, part.seq);
|
|
@@ -2441,6 +2528,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2441
2528
|
path
|
|
2442
2529
|
}, seq);
|
|
2443
2530
|
}
|
|
2531
|
+
for (const path of this.syncedDirs) if (!dirs.has(path)) this.receiveDirRemoved(path, seq);
|
|
2444
2532
|
this.scopeTaken = asked;
|
|
2445
2533
|
this.log(`took the snapshot at seq ${seq} in ${parts.length} batch${parts.length === 1 ? "" : "es"}: ${sent} files sent, ${present.size - sent} already held here${this.remote.size > 0 ? `, ${this.remote.size} only on the server` : ""}`);
|
|
2446
2534
|
this.takenUpTo(seq);
|
|
@@ -2456,6 +2544,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2456
2544
|
for (const path of await this.walk()) this.schedule(path);
|
|
2457
2545
|
for (const path of this.stalledDownloads) this.schedule(path);
|
|
2458
2546
|
this.stalledDownloads.clear();
|
|
2547
|
+
await this.queueRemovedDirs();
|
|
2459
2548
|
}
|
|
2460
2549
|
/**
|
|
2461
2550
|
* Ask the server whether it keeps binary files (blobSupport), once per connection, before its snapshot. A probe that
|
|
@@ -2618,7 +2707,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2618
2707
|
if (expected?.blob && expected.hash === disk.hash) this.expected.delete(path);
|
|
2619
2708
|
if (isBlob(this.cloud.get(path))) this.cloud.delete(path);
|
|
2620
2709
|
}
|
|
2621
|
-
if (forgotten.size > 0) await this.whenFree(() => underLock(this.root, () => writeKnown(this.root, this.knownFiles())));
|
|
2710
|
+
if (forgotten.size > 0) await this.whenFree(() => underLock(this.root, () => writeKnown(this.root, this.knownFiles(), this.knownDirs())));
|
|
2622
2711
|
return forgotten;
|
|
2623
2712
|
}
|
|
2624
2713
|
/** The server refused the upload, so it holds what it held: the binary files forgetBinaries forgot are known again. */
|
|
@@ -2844,6 +2933,9 @@ var SyncClient = class extends EventEmitter {
|
|
|
2844
2933
|
* now on (I4). A version only written here, still in the outbox, is never confirmed.
|
|
2845
2934
|
*/
|
|
2846
2935
|
confirm(path, disk) {
|
|
2936
|
+
if (disk !== null) {
|
|
2937
|
+
for (let at = path.lastIndexOf("/"); at > 0; at = path.lastIndexOf("/", at - 1)) if (this.syncsDir(path.slice(0, at))) this.knowDir(path.slice(0, at));
|
|
2938
|
+
}
|
|
2847
2939
|
const held = this.confirmed.get(path);
|
|
2848
2940
|
if (held === void 0 ? false : held === null ? disk === null : disk !== null && held.hash === disk.hash && (held.text !== void 0 || disk.text === void 0)) return;
|
|
2849
2941
|
this.confirmed.set(path, disk);
|
|
@@ -3065,7 +3157,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
3065
3157
|
const fetched = [];
|
|
3066
3158
|
for (const part of parts) for (const event of part.files) {
|
|
3067
3159
|
if (event.kind === "dir") {
|
|
3068
|
-
this.
|
|
3160
|
+
this.receiveDir(event.path, part.seq, false);
|
|
3069
3161
|
continue;
|
|
3070
3162
|
}
|
|
3071
3163
|
mentioned.add(event.path);
|
|
@@ -3843,6 +3935,88 @@ var SyncClient = class extends EventEmitter {
|
|
|
3843
3935
|
this.trace(`removal of ${path} changed before it was queued; reconciling it again`);
|
|
3844
3936
|
this.schedule(path);
|
|
3845
3937
|
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Queue the removal of each directory the client had here that is gone (syncedDirs), with nothing the client knows of
|
|
3940
|
+
* in it: no file the client left there and has not sent the removal of, no write in the outbox of a file there, which
|
|
3941
|
+
* goes first, and no file the server holds there that is not written here yet, nor one only on the server. The
|
|
3942
|
+
* deepest go first, so the server finds the ones in each removed already; one holding anything holds the ones above it
|
|
3943
|
+
* too. Each is checked again in the hold of the write lock that queues it, so a directory made again meanwhile stays.
|
|
3944
|
+
* Only once a snapshot was taken under the rules as they are, since what the server holds is unknown before.
|
|
3945
|
+
*/
|
|
3946
|
+
async queueRemovedDirs() {
|
|
3947
|
+
if (!this.synced || this.scopeStale || this.closing) return;
|
|
3948
|
+
const exists = (dir) => existsSync(join(this.paths.data, dir));
|
|
3949
|
+
const gone = [...this.syncedDirs].filter((dir) => this.syncsDir(dir) && !exists(dir));
|
|
3950
|
+
if (gone.length === 0) return;
|
|
3951
|
+
gone.sort((a, b) => b.split("/").length - a.split("/").length || comparePaths(a, b));
|
|
3952
|
+
const queued = [];
|
|
3953
|
+
await this.whenFree(() => this.editQueue((entries) => {
|
|
3954
|
+
const files = [
|
|
3955
|
+
...[...this.expected].flatMap(([path, disk]) => disk === null ? [] : [path]),
|
|
3956
|
+
...[...this.cloud].flatMap(([path, file]) => file === null ? [] : [path]),
|
|
3957
|
+
...this.remote.keys(),
|
|
3958
|
+
...this.awaiting,
|
|
3959
|
+
...entries.flatMap(entryPaths)
|
|
3960
|
+
];
|
|
3961
|
+
const held = /* @__PURE__ */ new Set();
|
|
3962
|
+
const hold = (dir) => {
|
|
3963
|
+
for (let at = dir.length; at > 0; at = dir.lastIndexOf("/", at - 1)) held.add(dir.slice(0, at));
|
|
3964
|
+
};
|
|
3965
|
+
for (const path of files) hold(path.slice(0, Math.max(0, path.lastIndexOf("/"))));
|
|
3966
|
+
for (const dir of gone) {
|
|
3967
|
+
if (held.has(dir) || exists(dir)) {
|
|
3968
|
+
hold(dir);
|
|
3969
|
+
continue;
|
|
3970
|
+
}
|
|
3971
|
+
if (entries.some((entry) => entry.kind === "rmdir" && entry.path === dir)) continue;
|
|
3972
|
+
entries.push({
|
|
3973
|
+
kind: "rmdir",
|
|
3974
|
+
path: dir,
|
|
3975
|
+
id: entryId()
|
|
3976
|
+
});
|
|
3977
|
+
queued.push(dir);
|
|
3978
|
+
}
|
|
3979
|
+
return queued.length > 0;
|
|
3980
|
+
}));
|
|
3981
|
+
if (queued.length === 0) return;
|
|
3982
|
+
this.log(`queued rmdir ${queued.join(", ")} (${this.queue.length} waiting)`);
|
|
3983
|
+
if (this.online) this.track(this.flushQueue());
|
|
3984
|
+
}
|
|
3985
|
+
/**
|
|
3986
|
+
* Queue the removal of the directories gone here a moment from now, once for every call in that moment; counted as
|
|
3987
|
+
* work under way meanwhile, so idle() waits for it.
|
|
3988
|
+
*/
|
|
3989
|
+
checkDirs() {
|
|
3990
|
+
if (this.closing || this.dirCheck) return;
|
|
3991
|
+
this.dirCheck = this.track(sleep$1(SETTLE_MS).then(() => {
|
|
3992
|
+
this.dirCheck = null;
|
|
3993
|
+
return this.queueRemovedDirs();
|
|
3994
|
+
}).catch((error) => this.log(`directories: ${error.message}`)));
|
|
3995
|
+
}
|
|
3996
|
+
/**
|
|
3997
|
+
* What the server answered to the removal of a directory: removed, or not there, it is known no more. Kept, as when
|
|
3998
|
+
* another environment has just put a file in it, it is made here again, as the server holds it, so it is not sent
|
|
3999
|
+
* again; the file comes as the server's broadcast. Refused, as by a server from before dir.remove (`unknown-operation`),
|
|
4000
|
+
* it is known no more either: the next snapshot makes it here again, as before, rather than it being sent again and again.
|
|
4001
|
+
*/
|
|
4002
|
+
async settleRmdir(entry, answer) {
|
|
4003
|
+
const { path } = entry;
|
|
4004
|
+
const response = answer;
|
|
4005
|
+
this.trace(`response ${path}/ ${response.ok ? response.value : `${response.reason}: ${response.message}`}`);
|
|
4006
|
+
if (!response.ok) {
|
|
4007
|
+
this.forgetDir(path);
|
|
4008
|
+
this.dirSeq.delete(path);
|
|
4009
|
+
this.info(path, `the directory was removed here and the sync server did not remove it (${response.message}); it comes back with the next snapshot`);
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
if (response.value === "kept") {
|
|
4013
|
+
for (const notice of response.notices) this.info(notice.path, `${notice.message} (removed here; the directory is back)`);
|
|
4014
|
+
await this.whenFree(() => this.locked(() => mkdirSync(join(this.paths.data, path), { recursive: true })));
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
this.forgetDir(path);
|
|
4018
|
+
if (response.files.length > 0) await this.delivery(path, response.seq);
|
|
4019
|
+
}
|
|
3846
4020
|
async settleRemove(entry, response) {
|
|
3847
4021
|
const { path } = entry;
|
|
3848
4022
|
if (!response.ok) return this.refuse(entry, [{
|
|
@@ -3920,7 +4094,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
3920
4094
|
hash: entry.hash,
|
|
3921
4095
|
blob: true
|
|
3922
4096
|
});
|
|
3923
|
-
else this.expected.set(entry.path, null);
|
|
4097
|
+
else if (entry.kind === "remove") this.expected.set(entry.path, null);
|
|
3924
4098
|
}
|
|
3925
4099
|
/**
|
|
3926
4100
|
* Hold the write lock for `fn`; work stop() abandoned gets an error ('abandoned after stop') instead, so it leaves
|
|
@@ -4379,7 +4553,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
4379
4553
|
if (at < 0) return false;
|
|
4380
4554
|
entries.splice(at, 1);
|
|
4381
4555
|
for (const later of entries.slice(at)) {
|
|
4382
|
-
if (later.kind === "op" || later.path !== entry.path || later.sent !== void 0 || later.base !== entry.hash) continue;
|
|
4556
|
+
if (later.kind === "op" || later.kind === "rmdir" || later.path !== entry.path || later.sent !== void 0 || later.base !== entry.hash) continue;
|
|
4383
4557
|
if (later.kind === "remove" && entry.base === null) continue;
|
|
4384
4558
|
later.base = entry.base;
|
|
4385
4559
|
}
|
|
@@ -4422,6 +4596,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
4422
4596
|
this.renumbering.delete(entry.id);
|
|
4423
4597
|
if (renumbered) this.log(`renumbered ${renumbered.map((r) => `${r.from} → ${r.to}`).join(", ")}`);
|
|
4424
4598
|
for (const path of entryPaths(entry)) this.schedule(path);
|
|
4599
|
+
if (entry.kind !== "rmdir") this.checkDirs();
|
|
4425
4600
|
this.emit("settled", entry.id);
|
|
4426
4601
|
}
|
|
4427
4602
|
/**
|
|
@@ -4494,7 +4669,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
4494
4669
|
}
|
|
4495
4670
|
if (!response.ok && response.reason === "uncertain") {
|
|
4496
4671
|
const paths = entryPaths(entry);
|
|
4497
|
-
this.problem({
|
|
4672
|
+
if (entry.kind !== "rmdir") this.problem({
|
|
4498
4673
|
kind: "uncertain",
|
|
4499
4674
|
path: paths[0] ?? "",
|
|
4500
4675
|
message: `${request.op} of ${paths.join(", ")} was sent before without an answer, and the sync server no longer keeps its answers from then (${response.message}); it is sent again as a new request and may apply twice, so check the result for a duplicate`
|
|
@@ -4505,8 +4680,8 @@ var SyncClient = class extends EventEmitter {
|
|
|
4505
4680
|
}
|
|
4506
4681
|
/**
|
|
4507
4682
|
* The request an outbox entry sends, made when it is first sent, from the entry alone: an operation on the current
|
|
4508
|
-
* protocol, a deletion with the time it is first sent, an Edit's replacement to be made again on
|
|
4509
|
-
* version, an edit as patchRequest makes it. Null for an edit that cannot be sent: its base is a write the server
|
|
4683
|
+
* protocol, a deletion with the time it is first sent, a directory's removal, an Edit's replacement to be made again on
|
|
4684
|
+
* the server's latest version, an edit as patchRequest makes it. Null for an edit that cannot be sent: its base is a write the server
|
|
4510
4685
|
* refused or did not take as it was, so its base never became known, or moving it onto the server's merge overlaps.
|
|
4511
4686
|
* Every write ahead of it was answered, so a base still unknown never will be.
|
|
4512
4687
|
*/
|
|
@@ -4529,6 +4704,11 @@ var SyncClient = class extends EventEmitter {
|
|
|
4529
4704
|
size: entry.size,
|
|
4530
4705
|
base: entry.base
|
|
4531
4706
|
};
|
|
4707
|
+
if (entry.kind === "rmdir") return {
|
|
4708
|
+
protocol: 3,
|
|
4709
|
+
op: "dir.remove",
|
|
4710
|
+
path: entry.path
|
|
4711
|
+
};
|
|
4532
4712
|
if (entry.replace) return {
|
|
4533
4713
|
protocol: 3,
|
|
4534
4714
|
op: "text.replace",
|
|
@@ -4675,6 +4855,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
4675
4855
|
last: entry.replaced
|
|
4676
4856
|
}, request.text);
|
|
4677
4857
|
else if (entry.kind === "blob") await this.settleBlob(entry, response);
|
|
4858
|
+
else if (entry.kind === "rmdir") await this.settleRmdir(entry, response);
|
|
4678
4859
|
return true;
|
|
4679
4860
|
} finally {
|
|
4680
4861
|
for (const path of paths) {
|
|
@@ -4827,6 +5008,26 @@ var SyncClient = class extends EventEmitter {
|
|
|
4827
5008
|
text
|
|
4828
5009
|
});
|
|
4829
5010
|
}
|
|
5011
|
+
for (const path of [...seen.keys(), ...binary.map(([path]) => path)]) this.pruneMoved(path, renumberPath(path, renumbered));
|
|
5012
|
+
}
|
|
5013
|
+
/**
|
|
5014
|
+
* Remove the directories above `from` that its move to `to` emptied, the deepest first, up to the first one that still
|
|
5015
|
+
* holds anything or is above `to` as well. One the server holds goes on the server too, as any directory gone here
|
|
5016
|
+
* (queueRemovedDirs). It does not take the write lock: its caller holds it.
|
|
5017
|
+
*/
|
|
5018
|
+
pruneMoved(from, to) {
|
|
5019
|
+
if (from === to) return;
|
|
5020
|
+
for (let at = from.lastIndexOf("/"); at > 0; at = from.lastIndexOf("/", at - 1)) {
|
|
5021
|
+
const dir = from.slice(0, at);
|
|
5022
|
+
if (to.startsWith(`${dir}/`)) return;
|
|
5023
|
+
try {
|
|
5024
|
+
if (!this.removeEmptyDir(dir)) return;
|
|
5025
|
+
} catch (error) {
|
|
5026
|
+
this.log(`${dir}/: could not remove it after renumbering: ${error.message}`);
|
|
5027
|
+
return;
|
|
5028
|
+
}
|
|
5029
|
+
this.trace(`dir ${dir} emptied by renumbering, removed here`);
|
|
5030
|
+
}
|
|
4830
5031
|
}
|
|
4831
5032
|
commit(storage, changes) {
|
|
4832
5033
|
for (const change of changes) if (change.kind === "write") storage.write(change.path, change.text);
|
|
@@ -5159,7 +5360,8 @@ var SyncClient = class extends EventEmitter {
|
|
|
5159
5360
|
}
|
|
5160
5361
|
/**
|
|
5161
5362
|
* Compare every synced file's size, modification time and inode with the last scan and reconcile what changed,
|
|
5162
|
-
* plus every file expected on disk that is gone
|
|
5363
|
+
* plus every file expected on disk that is gone, and queue the removal of the directories gone here
|
|
5364
|
+
* (queueRemovedDirs). A file modified in the last moment may still be being written,
|
|
5163
5365
|
* so it waits for the next scan unless `immediate`. `baseline` only records what is there. An immediate scan waits for
|
|
5164
5366
|
* the reconciles it started unless `settle` is false, for a caller that waits for them itself (flushTool).
|
|
5165
5367
|
*/
|
|
@@ -5180,6 +5382,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
5180
5382
|
this.signatures.delete(path);
|
|
5181
5383
|
this.schedule(path);
|
|
5182
5384
|
}
|
|
5385
|
+
await this.queueRemovedDirs();
|
|
5183
5386
|
if (settle) await Promise.allSettled(this.chains.values());
|
|
5184
5387
|
}
|
|
5185
5388
|
/**
|
|
@@ -5197,6 +5400,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
5197
5400
|
}, SETTLE_MS));
|
|
5198
5401
|
return;
|
|
5199
5402
|
}
|
|
5403
|
+
if (this.syncedDirs.has(path)) this.checkDirs();
|
|
5200
5404
|
const rules = path === ".gitignore" || path.endsWith(`/.gitignore`);
|
|
5201
5405
|
if (!rules && !this.syncs(path)) return;
|
|
5202
5406
|
this.trace(`watched ${path}`);
|
|
@@ -5296,13 +5500,18 @@ var SyncClient = class extends EventEmitter {
|
|
|
5296
5500
|
for (const [path, disk] of this.confirmed) if (disk) files[path] = disk.blob ? { hash: `${BLOB_PREFIX}${disk.hash}` } : disk;
|
|
5297
5501
|
return files;
|
|
5298
5502
|
}
|
|
5503
|
+
/** The directories known.json names (syncedDirs), in path order. */
|
|
5504
|
+
knownDirs() {
|
|
5505
|
+
return [...this.syncedDirs].sort(comparePaths);
|
|
5506
|
+
}
|
|
5299
5507
|
saveKnownNow(final = false) {
|
|
5300
5508
|
if (this.abandoned && !final) return;
|
|
5301
5509
|
clearTimeout(this.knownTimer);
|
|
5302
5510
|
this.knownTimer = void 0;
|
|
5303
5511
|
const files = this.knownFiles();
|
|
5512
|
+
const dirs = this.knownDirs();
|
|
5304
5513
|
try {
|
|
5305
|
-
underLock(this.root, () => writeKnown(this.root, files));
|
|
5514
|
+
underLock(this.root, () => writeKnown(this.root, files, dirs));
|
|
5306
5515
|
} catch (error) {
|
|
5307
5516
|
this.log(`could not save local/sync/known.json: ${error.message}`);
|
|
5308
5517
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { P as configSchema } from "./dist-zSabglPq.js";
|
|
2
2
|
import { $ as linksInto, At as WriteError, C as HANDLED_FILE, D as loadSessions, E as readNotices$1, I as attributeTrace, J as loadConfig, L as traceTranscript, O as readSession, Ot as ConflictError, Q as mainlineEdges, R as transcriptTitle, T as parseWarn, X as sectionText, Y as loadHub, Z as dependencyGraph, _ as setArchivedMany, a as openInApp, g as ARCHIVE_KINDS, k as transcriptPath, kt as DATA_DIR, nt as remoteMembers, q as HubError, st as readYaml, tt as refIndex, u as groupPlan, w as markNoticesHandled } from "./host-BAMZmRy8.js";
|
|
3
3
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
4
|
-
import { A as isSynced, _ as readDaemon, a as remoteMode, b as readRemote, h as callDaemon, j as syncPaths, k as SYNC_DIR, m as DaemonUnavailable, n as hubWriter, r as writtenLines, y as watchTree } from "./operation-
|
|
5
|
-
import { c as realRoot, f as CLI_VERSION, s as readUiRecord } from "./launch-
|
|
4
|
+
import { A as isSynced, _ as readDaemon, a as remoteMode, b as readRemote, h as callDaemon, j as syncPaths, k as SYNC_DIR, m as DaemonUnavailable, n as hubWriter, r as writtenLines, y as watchTree } from "./operation-2wx2ex6x.js";
|
|
5
|
+
import { c as realRoot, f as CLI_VERSION, s as readUiRecord } from "./launch-DosYhbwc.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
|
|
8
8
|
import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
|