mainflow 0.32.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +11 -5
- package/dist/hooks.js +2 -2
- package/dist/{host-CdB_vDY_.js → host-C7GKIjSc.js} +23 -1
- package/dist/host.js +1 -1
- package/dist/{launch-Bmzqx-TI.js → launch-6839iAsc.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/{operation-CuWhNoIa.js → operation-C0dUHUHg.js} +347 -62
- package/dist/{ui-rwBrbIIF.js → ui-Bx2wUBnA.js} +3 -3
- package/dist/web/assets/{index-B-_uZYXu.js → index-D2iMe1-S.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +2 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as ACCESS_HEADERS, F as renumberFiles, I as renumberPath, L as renumberText, N as applyOffline, P as renumberRequest, Q as nodeStorage, R as scopeOfPath, X as nowMinute,
|
|
1
|
+
import { $ as ACCESS_HEADERS, F as renumberFiles, I as renumberPath, L as renumberText, N as applyOffline, P as renumberRequest, Q as nodeStorage, R as scopeOfPath, X as nowMinute, at as requestTime, dt as DATA_DIR, et as MAX_FILE_BYTES, ft as WriteError, it as exceedsFileLimit, mt as splitFrontMatter, nt as SOCKET_MAX_AGE_CLOSE, ot as textBytes, pt as contentHash, rt as comparePaths, tt as ROUTES, ut as ConflictError, z as applyOperation } from "./host-C7GKIjSc.js";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -12,6 +12,7 @@ import { Agent as Agent$1, request as request$1 } from "node:https";
|
|
|
12
12
|
import { EventEmitter } from "node:events";
|
|
13
13
|
import { appendFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
14
14
|
import { diff3Merge } from "node-diff3";
|
|
15
|
+
import ignore from "ignore";
|
|
15
16
|
//#region ../sync/dist/paths.js
|
|
16
17
|
/** The sync client's own directory, inside local/ so it is neither synced nor committed. */
|
|
17
18
|
const SYNC_DIR = "local/sync";
|
|
@@ -373,7 +374,8 @@ var ServerApi = class {
|
|
|
373
374
|
* text the client holds of each file, so the server sends only the files that differ, and names the ones of `have` it
|
|
374
375
|
* does not hold as removed; each batch is sent only the part of `have` in its own range. A path of `have` that no
|
|
375
376
|
* batch mentions is a file the server holds with that text. The batches are in order, each with its seq; one whose seq
|
|
376
|
-
* is below the batch before, as when the server's storage was replaced meanwhile, starts the snapshot over.
|
|
377
|
+
* is below the batch before, as when the server's storage was replaced meanwhile, starts the snapshot over. A path of
|
|
378
|
+
* `have` the server never held a file at comes as `unknown` rather than `removed`, from a server that tells them apart.
|
|
377
379
|
* `listOnly` asks for the files under those prefixes as `listed`, without their text. A part of the hub alone is taken
|
|
378
380
|
* from `after` on, and ends with the batch that covers `through`; only the paths of that range are told of.
|
|
379
381
|
*/
|
|
@@ -390,6 +392,7 @@ var ServerApi = class {
|
|
|
390
392
|
const { seq, files, next } = await this.batch({
|
|
391
393
|
protocol: 3,
|
|
392
394
|
have: range,
|
|
395
|
+
unknown: true,
|
|
393
396
|
...after === void 0 ? {} : { after },
|
|
394
397
|
...options.listOnly?.length ? { listOnly: options.listOnly } : {}
|
|
395
398
|
});
|
|
@@ -561,6 +564,135 @@ function mergeThreeWay(current, base, incoming) {
|
|
|
561
564
|
};
|
|
562
565
|
}
|
|
563
566
|
//#endregion
|
|
567
|
+
//#region ../sync/dist/scope.js
|
|
568
|
+
/** The name of a file of ignore rules. */
|
|
569
|
+
const GITIGNORE = ".gitignore";
|
|
570
|
+
const decoder$1 = new TextDecoder("utf-8", { fatal: true });
|
|
571
|
+
/** The text of the file at `file`, or null when there is none or it cannot be read. */
|
|
572
|
+
function readText(file) {
|
|
573
|
+
try {
|
|
574
|
+
return readFileSync(file, "utf8");
|
|
575
|
+
} catch {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/** The hub's ignore rules, by the directory of each .gitignore, as last read. */
|
|
580
|
+
var SyncScope = class {
|
|
581
|
+
/** Each .gitignore's text and rules by its directory, relative to .mainflow/ with `/`, '' for .mainflow/ itself */
|
|
582
|
+
rules = /* @__PURE__ */ new Map();
|
|
583
|
+
/** Whether each path, a directory's ending in `/`, is ignored under the rules as they are */
|
|
584
|
+
decided = /* @__PURE__ */ new Map();
|
|
585
|
+
changes = 0;
|
|
586
|
+
/** How many times the rules changed: a caller that noted it tells whether they changed since. */
|
|
587
|
+
get generation() {
|
|
588
|
+
return this.changes;
|
|
589
|
+
}
|
|
590
|
+
/** The text of `dir`'s .gitignore as the rules have it, null for none. */
|
|
591
|
+
text(dir) {
|
|
592
|
+
return this.rules.get(dir)?.text ?? null;
|
|
593
|
+
}
|
|
594
|
+
/** Take `text` as the .gitignore of `dir`, null for none; true when that changes the rules. */
|
|
595
|
+
set(dir, text) {
|
|
596
|
+
if ((this.rules.get(dir)?.text ?? null) === text) return false;
|
|
597
|
+
if (text === null) this.rules.delete(dir);
|
|
598
|
+
else this.rules.set(dir, {
|
|
599
|
+
text,
|
|
600
|
+
matcher: ignore().add(text)
|
|
601
|
+
});
|
|
602
|
+
this.decided.clear();
|
|
603
|
+
this.changes++;
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
/** Forget the rules of every directory but `dirs`, as when a scan of the whole hub no longer finds the others. */
|
|
607
|
+
keepOnly(dirs) {
|
|
608
|
+
for (const dir of this.rules.keys()) if (!dirs.has(dir)) this.set(dir, null);
|
|
609
|
+
}
|
|
610
|
+
/** Whether the file at `path`, relative to .mainflow/, is synced: by the sync client's own rules, and not ignored. */
|
|
611
|
+
synced(path) {
|
|
612
|
+
return isSynced(path) && !this.ignored(path, false);
|
|
613
|
+
}
|
|
614
|
+
/** Whether a scan walks into the directory at `path`: one that may hold synced files, and not ignored. */
|
|
615
|
+
walks(path) {
|
|
616
|
+
return mayHoldSynced(path) && !this.ignored(path, true);
|
|
617
|
+
}
|
|
618
|
+
/** Whether git leaves the file or directory (`dir`) at `path` out: it, or a directory above it, is ignored. */
|
|
619
|
+
ignored(path, dir) {
|
|
620
|
+
if (path === "local" || path.startsWith("local/")) return false;
|
|
621
|
+
const parts = path.split("/");
|
|
622
|
+
if (parts.some((part) => part === "" || part === "." || part === "..")) return true;
|
|
623
|
+
const key = dir ? `${path}/` : path;
|
|
624
|
+
const known = this.decided.get(key);
|
|
625
|
+
if (known !== void 0) return known;
|
|
626
|
+
let out = false;
|
|
627
|
+
for (let n = 1; n <= parts.length && !out; n++) out = this.matches(parts, n, n < parts.length || dir);
|
|
628
|
+
this.decided.set(key, out);
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Whether the rules ignore the first `n` parts of a path, a directory's when `dir`: each .gitignore above it in turn,
|
|
633
|
+
* from .mainflow/ down, the last one with a pattern that matches deciding.
|
|
634
|
+
*/
|
|
635
|
+
matches(parts, n, dir) {
|
|
636
|
+
if (parts[n - 1] === ".git") return true;
|
|
637
|
+
let out = false;
|
|
638
|
+
for (let depth = 0; depth < n; depth++) {
|
|
639
|
+
const rules = this.rules.get(parts.slice(0, depth).join("/"));
|
|
640
|
+
if (!rules) continue;
|
|
641
|
+
const test = rules.matcher.test(`${parts.slice(depth, n).join("/")}${dir ? "/" : ""}`);
|
|
642
|
+
if (test.ignored) out = true;
|
|
643
|
+
else if (test.unignored) out = false;
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Read every .gitignore under `data`, the hub's .mainflow/, again, and return the synced files there; it walks only into
|
|
649
|
+
* the directories the rules leave in, as git does. Synchronous, for a start and for `mainflow check`.
|
|
650
|
+
*/
|
|
651
|
+
load(data) {
|
|
652
|
+
const files = [];
|
|
653
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
654
|
+
const visit = (dir) => {
|
|
655
|
+
let entries;
|
|
656
|
+
try {
|
|
657
|
+
entries = readdirSync(join(data, dir), { withFileTypes: true });
|
|
658
|
+
} catch {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
dirs.add(dir);
|
|
662
|
+
this.set(dir, entries.some((entry) => entry.name === ".gitignore" && entry.isFile()) ? readText(join(data, dir, GITIGNORE)) : null);
|
|
663
|
+
for (const entry of entries) {
|
|
664
|
+
const path = dir ? `${dir}/${entry.name}` : entry.name;
|
|
665
|
+
if (entry.isDirectory()) {
|
|
666
|
+
if (this.walks(path)) visit(path);
|
|
667
|
+
} else if (entry.isFile() && this.synced(path)) files.push(path);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
visit("");
|
|
671
|
+
this.keepOnly(dirs);
|
|
672
|
+
return files;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
/**
|
|
676
|
+
* The synced files of the hub at `root` larger than the sync server keeps (MAX_FILE_BYTES): UTF-8 text the hub's
|
|
677
|
+
* .gitignore files leave in. The sync client leaves them out of sync; `mainflow check` warns of them.
|
|
678
|
+
*/
|
|
679
|
+
function oversizedFiles(root) {
|
|
680
|
+
const data = join(root, DATA_DIR);
|
|
681
|
+
const out = [];
|
|
682
|
+
for (const path of new SyncScope().load(data)) {
|
|
683
|
+
const file = join(data, path);
|
|
684
|
+
try {
|
|
685
|
+
if (statSync(file).size <= 19e5) continue;
|
|
686
|
+
const bytes = textBytes(decoder$1.decode(readFileSync(file)));
|
|
687
|
+
if (bytes > 19e5) out.push({
|
|
688
|
+
path,
|
|
689
|
+
bytes
|
|
690
|
+
});
|
|
691
|
+
} catch {}
|
|
692
|
+
}
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
//#endregion
|
|
564
696
|
//#region ../sync/dist/state.js
|
|
565
697
|
/** Whether `entry` waits for the server: not one it refused. */
|
|
566
698
|
const waiting = (entry) => entry.refused === void 0;
|
|
@@ -1094,14 +1226,17 @@ function patchRequest(entry) {
|
|
|
1094
1226
|
text
|
|
1095
1227
|
};
|
|
1096
1228
|
}
|
|
1097
|
-
/**
|
|
1229
|
+
/**
|
|
1230
|
+
* Whether the server holds any file, by a snapshot taken with `held` named: one it sent, or one of `held` it did not name
|
|
1231
|
+
* removed or unknown.
|
|
1232
|
+
*/
|
|
1098
1233
|
function holdsFiles(parts, held) {
|
|
1099
|
-
const
|
|
1234
|
+
const missing = /* @__PURE__ */ new Set();
|
|
1100
1235
|
for (const part of parts) for (const event of part.files) {
|
|
1101
1236
|
if (event.kind === "file" || event.kind === "listed") return true;
|
|
1102
|
-
if (event.kind === "removed")
|
|
1237
|
+
if (event.kind === "removed" || event.kind === "unknown") missing.add(event.path);
|
|
1103
1238
|
}
|
|
1104
|
-
return [...held.keys()].some((path) => !
|
|
1239
|
+
return [...held.keys()].some((path) => !missing.has(path));
|
|
1105
1240
|
}
|
|
1106
1241
|
var SyncClient = class extends EventEmitter {
|
|
1107
1242
|
opts;
|
|
@@ -1141,6 +1276,22 @@ var SyncClient = class extends EventEmitter {
|
|
|
1141
1276
|
copied = false;
|
|
1142
1277
|
/** The server has no copy of the hub, which this environment, downloading on demand, does not upload (snapshotNow) */
|
|
1143
1278
|
uploadWanted = false;
|
|
1279
|
+
/** Why the server refused the upload of this environment's copy, as last reported (snapshotNow); null once it holds the hub */
|
|
1280
|
+
uploadRefusal = null;
|
|
1281
|
+
/** The hub's .gitignore files, which leave a file out of sync as they leave it out of git (src/scope.ts) */
|
|
1282
|
+
scope = new SyncScope();
|
|
1283
|
+
/**
|
|
1284
|
+
* The generation of the rules (SyncScope.generation) the last snapshot was asked for under. A file the rules left out
|
|
1285
|
+
* then was not taken in, whatever the server sent, nor a broadcast of it since; rules changed after it may let such a
|
|
1286
|
+
* file in, so the snapshot is taken again (walk), and a file not taken in from the server waits for it (scopeStale).
|
|
1287
|
+
*/
|
|
1288
|
+
scopeTaken = -1;
|
|
1289
|
+
/**
|
|
1290
|
+
* The paths the server last named `unknown`: it holds no file there and never removed one. A file here at such a path
|
|
1291
|
+
* is new to the server, whatever known.json says, as when another environment uploaded the hub to a new server
|
|
1292
|
+
* without it; it is sent as new, never removed here. Only `removed` removes a file here.
|
|
1293
|
+
*/
|
|
1294
|
+
unheld = /* @__PURE__ */ new Set();
|
|
1144
1295
|
/** The hub's seq of each path's version in `cloud`: a version from the server with a seq no higher is not newer */
|
|
1145
1296
|
fileSeq = /* @__PURE__ */ new Map();
|
|
1146
1297
|
/** The hub's seq up to which the client has taken in every change: the last snapshot and the broadcasts after it */
|
|
@@ -1151,7 +1302,8 @@ var SyncClient = class extends EventEmitter {
|
|
|
1151
1302
|
inflight = /* @__PURE__ */ new Map();
|
|
1152
1303
|
chains = /* @__PURE__ */ new Map();
|
|
1153
1304
|
work = /* @__PURE__ */ new Set();
|
|
1154
|
-
|
|
1305
|
+
/** The files left out of sync and reported, by kind, path and content hash, so each is reported once */
|
|
1306
|
+
skipped = /* @__PURE__ */ new Set();
|
|
1155
1307
|
/**
|
|
1156
1308
|
* The files only on the server, by the hash of the server's text: artifacts of closed mainlines this client never
|
|
1157
1309
|
* had on disk and has not downloaded (onDemand). remote.json keeps them.
|
|
@@ -1241,6 +1393,20 @@ var SyncClient = class extends EventEmitter {
|
|
|
1241
1393
|
get online() {
|
|
1242
1394
|
return this.connected && !this.stopped;
|
|
1243
1395
|
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Whether the file at `path` is synced: by the sync client's own rules (src/paths.ts) and the hub's .gitignore files as
|
|
1398
|
+
* last read (src/scope.ts). Anything else is left as it is here, whatever the server holds.
|
|
1399
|
+
*/
|
|
1400
|
+
syncs(path) {
|
|
1401
|
+
return this.scope.synced(path);
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Whether the hub's .gitignore files changed since the last snapshot was asked for: a file they let in since may be on
|
|
1405
|
+
* the server although the client took nothing of it in.
|
|
1406
|
+
*/
|
|
1407
|
+
get scopeStale() {
|
|
1408
|
+
return this.scope.generation !== this.scopeTaken;
|
|
1409
|
+
}
|
|
1244
1410
|
/** Writes waiting in the outbox for the server: not the ones it refused, which wait for nothing (src/state.ts). */
|
|
1245
1411
|
get queued() {
|
|
1246
1412
|
return this.queue.filter(waiting).length;
|
|
@@ -1289,6 +1455,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1289
1455
|
this.info("", `atomic exchange unavailable, writing with a rename: ${swapper.reason}`);
|
|
1290
1456
|
}
|
|
1291
1457
|
}
|
|
1458
|
+
this.scope.load(this.paths.data);
|
|
1292
1459
|
await this.restoreKnown();
|
|
1293
1460
|
this.restoreRemote();
|
|
1294
1461
|
await this.whenFree(() => this.syncQueue());
|
|
@@ -1424,7 +1591,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1424
1591
|
this.connected = true;
|
|
1425
1592
|
this.rejoining = false;
|
|
1426
1593
|
this.current = false;
|
|
1427
|
-
if (!this.uploadWanted) this.backoff = 500;
|
|
1594
|
+
if (!this.uploadWanted && this.uploadRefusal === null) this.backoff = 500;
|
|
1428
1595
|
this.lastPong = Date.now();
|
|
1429
1596
|
clearInterval(this.pingTimer);
|
|
1430
1597
|
this.pingTimer = setInterval(() => this.ping(ws), 15e3);
|
|
@@ -1562,10 +1729,10 @@ var SyncClient = class extends EventEmitter {
|
|
|
1562
1729
|
*/
|
|
1563
1730
|
receive(event, seq) {
|
|
1564
1731
|
if (event.kind === "dir") {
|
|
1565
|
-
if (event.path.split("/")[0] !== "local" || event.path === "local/sessions") this.track(mkdir(join(this.paths.data, event.path), { recursive: true }));
|
|
1732
|
+
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 }));
|
|
1566
1733
|
return;
|
|
1567
1734
|
}
|
|
1568
|
-
if (!
|
|
1735
|
+
if (!this.syncs(event.path)) return;
|
|
1569
1736
|
const text = event.kind === "file" ? event.text : null;
|
|
1570
1737
|
const held = this.fileSeq.get(event.path) ?? -1;
|
|
1571
1738
|
if (seq <= held) {
|
|
@@ -1573,6 +1740,8 @@ var SyncClient = class extends EventEmitter {
|
|
|
1573
1740
|
return;
|
|
1574
1741
|
}
|
|
1575
1742
|
this.fileSeq.set(event.path, seq);
|
|
1743
|
+
if (event.kind === "unknown") this.unheld.add(event.path);
|
|
1744
|
+
else this.unheld.delete(event.path);
|
|
1576
1745
|
if (this.remote.has(event.path) && !this.awaiting.has(event.path) && (text === null || this.staysRemote(event.path))) {
|
|
1577
1746
|
this.trace(`remote ${event.path} ${short(text)} seq ${seq}`);
|
|
1578
1747
|
if (text === null) this.remote.delete(event.path);
|
|
@@ -1630,9 +1799,10 @@ var SyncClient = class extends EventEmitter {
|
|
|
1630
1799
|
* reconciled: the server broadcasts a change after storing it, so a snapshot asked for later holds it. One that
|
|
1631
1800
|
* still does not, and brought nothing newer, is asked for again after a pause; after STALLED_SNAPSHOTS of them the
|
|
1632
1801
|
* server is taken to have lost those changes, as when its storage was replaced: the versions the client holds from
|
|
1633
|
-
* after the gap are forgotten and the snapshot is taken once more, as it is.
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1802
|
+
* after the gap are forgotten and the snapshot is taken once more, as it is. So is it when the hub's .gitignore files
|
|
1803
|
+
* changed after it was asked for (scopeStale): a file they let in since was not taken in. The connection has its
|
|
1804
|
+
* snapshot once one ends while it is open (I1); one that fails leaves it without, and the client reconnects to take a
|
|
1805
|
+
* fresh one, rather than send on the snapshot of an earlier connection.
|
|
1636
1806
|
*/
|
|
1637
1807
|
resync() {
|
|
1638
1808
|
if (this.closing) return Promise.resolve();
|
|
@@ -1661,7 +1831,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1661
1831
|
this.log(`missed the hub's changes ${missing} while taking the snapshot; reloading it`);
|
|
1662
1832
|
if (stalled > 0) await sleep$1(250 * 2 ** (stalled - 1));
|
|
1663
1833
|
this.resyncAgain = true;
|
|
1664
|
-
} while (this.resyncAgain && !this.closing);
|
|
1834
|
+
} while ((this.resyncAgain || this.scopeStale) && !this.closing);
|
|
1665
1835
|
this.current = this.connected && !this.closing;
|
|
1666
1836
|
} catch (error) {
|
|
1667
1837
|
this.current = false;
|
|
@@ -1680,8 +1850,14 @@ var SyncClient = class extends EventEmitter {
|
|
|
1680
1850
|
* it comes as a broadcast, and the first batch's seq is the one the whole snapshot is as of. A file the server sent,
|
|
1681
1851
|
* or named removed, is taken at its batch's seq; a file it left out holds what the client named, as of the first
|
|
1682
1852
|
* batch's seq; a file neither named nor sent is not on the server. A broadcast arriving meanwhile may be older or
|
|
1683
|
-
* newer than either; each file keeps whichever version has the higher seq. A server
|
|
1684
|
-
*
|
|
1853
|
+
* newer than either; each file keeps whichever version has the higher seq. A path the server never held a file at
|
|
1854
|
+
* (`unknown`) is one it has no file at: a file here, even one never synced or one known.json names, is sent as new and
|
|
1855
|
+
* never removed (reconcile, unheld). What the
|
|
1856
|
+
* client does not sync (syncs), as a file the hub's .gitignore leaves out, is not taken in, whatever the server sends.
|
|
1857
|
+
*
|
|
1858
|
+
* A server without the hub first gets the local one. When it refuses the upload and still holds no file, the snapshot
|
|
1859
|
+
* fails without taking anything in: an empty hub would tell a copy without known.json to remove every file it holds.
|
|
1860
|
+
* The writer is told why once, and the next connection tries again.
|
|
1685
1861
|
*
|
|
1686
1862
|
* With onDemand, the artifacts of mainlines not known to be open come `listed` (listOnly), and the files only on the
|
|
1687
1863
|
* server are named by their hash, so one that has not changed is not mentioned. A listed file this client has no
|
|
@@ -1698,14 +1874,20 @@ var SyncClient = class extends EventEmitter {
|
|
|
1698
1874
|
let held;
|
|
1699
1875
|
let have;
|
|
1700
1876
|
let before;
|
|
1701
|
-
|
|
1877
|
+
/** Whether the hub was uploaded, and why the server refused the upload; asked again, the snapshot may hold the hub */
|
|
1878
|
+
let uploaded = false;
|
|
1879
|
+
let refusal = null;
|
|
1880
|
+
/** The rules' generation the snapshot is asked for under: what it holds is taken in as they leave it in */
|
|
1881
|
+
let asked;
|
|
1882
|
+
for (;;) {
|
|
1702
1883
|
before = this.hubSeq;
|
|
1703
1884
|
held = await this.holdings();
|
|
1704
1885
|
for (const path of this.remote.keys()) if (held.has(path) || this.expected.get(path) !== void 0) this.remote.delete(path);
|
|
1705
1886
|
have = new Map([...held].map(([path, text]) => [path, this.hashOf(path, text)]));
|
|
1706
1887
|
for (const [path, hash] of this.remote) have.set(path, hash);
|
|
1888
|
+
asked = this.scope.generation;
|
|
1707
1889
|
parts = await this.api.snapshot(have, { listOnly: this.listOnly(this.copied && !this.synced) });
|
|
1708
|
-
if (
|
|
1890
|
+
if (uploaded || holdsFiles(parts, have)) break;
|
|
1709
1891
|
if (this.opts.settings.onDemand === true) {
|
|
1710
1892
|
if (!this.uploadWanted) {
|
|
1711
1893
|
this.uploadWanted = true;
|
|
@@ -1717,10 +1899,18 @@ var SyncClient = class extends EventEmitter {
|
|
|
1717
1899
|
}
|
|
1718
1900
|
throw new Error("the sync server has no copy of the hub, and this environment downloads on demand, so it waits for one that keeps every file to upload it");
|
|
1719
1901
|
}
|
|
1720
|
-
if (
|
|
1902
|
+
if (refusal !== null) {
|
|
1903
|
+
this.refusedUpload(refusal);
|
|
1904
|
+
throw new Error(`the sync server has no copy of the hub and refused its upload (${refusal}); nothing was taken in, and the next connection tries again`);
|
|
1905
|
+
}
|
|
1906
|
+
const answer = await this.uploadIfEmpty();
|
|
1907
|
+
if (answer === null) break;
|
|
1908
|
+
if (answer.ok) uploaded = true;
|
|
1909
|
+
else refusal = answer.message;
|
|
1721
1910
|
}
|
|
1722
|
-
if (this.uploadWanted) {
|
|
1911
|
+
if (this.uploadWanted || this.uploadRefusal !== null) {
|
|
1723
1912
|
this.uploadWanted = false;
|
|
1913
|
+
this.uploadRefusal = null;
|
|
1724
1914
|
this.backoff = 500;
|
|
1725
1915
|
}
|
|
1726
1916
|
if (this.closing) return;
|
|
@@ -1738,6 +1928,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1738
1928
|
this.snapshotAnswers.clear();
|
|
1739
1929
|
const fromCopy = this.copied && !this.synced;
|
|
1740
1930
|
for (const part of parts) for (const event of part.files) {
|
|
1931
|
+
if (event.kind !== "dir" && !this.syncs(event.path)) continue;
|
|
1741
1932
|
if (event.kind === "listed") {
|
|
1742
1933
|
named.add(event.path);
|
|
1743
1934
|
if (this.mayStay(event.path, held)) this.keepRemote(event.path, event.hash, part.seq);
|
|
@@ -1772,10 +1963,11 @@ var SyncClient = class extends EventEmitter {
|
|
|
1772
1963
|
text
|
|
1773
1964
|
}, seq);
|
|
1774
1965
|
}
|
|
1775
|
-
for (const path of /* @__PURE__ */ new Set([...this.cloud.keys(), ...this.expected.keys()])) if (!present.has(path) && !named.has(path) &&
|
|
1966
|
+
for (const path of /* @__PURE__ */ new Set([...this.cloud.keys(), ...this.expected.keys()])) if (!present.has(path) && !named.has(path) && this.syncs(path)) this.receive({
|
|
1776
1967
|
kind: "removed",
|
|
1777
1968
|
path
|
|
1778
1969
|
}, seq);
|
|
1970
|
+
this.scopeTaken = asked;
|
|
1779
1971
|
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` : ""}`);
|
|
1780
1972
|
this.takenUpTo(seq);
|
|
1781
1973
|
this.synced = true;
|
|
@@ -1789,16 +1981,17 @@ var SyncClient = class extends EventEmitter {
|
|
|
1789
1981
|
* The text the client holds of each synced file, which a snapshot request names by hash so the server leaves out what
|
|
1790
1982
|
* is the same (SnapshotRequest.have): the server's version this run last took in, else the version known.json names,
|
|
1791
1983
|
* else the file on disk, as in a hub cloned with git that never synced here. Only a text the client has is named,
|
|
1792
|
-
* since a file the server leaves out holds the text named for it
|
|
1984
|
+
* since a file the server leaves out holds the text named for it; a file larger than the server keeps is not synced,
|
|
1985
|
+
* so it is not named either.
|
|
1793
1986
|
*/
|
|
1794
1987
|
async holdings() {
|
|
1795
1988
|
const held = /* @__PURE__ */ new Map();
|
|
1796
|
-
for (const [path, text] of this.cloud) if (typeof text === "string" &&
|
|
1797
|
-
for (const [path, disk] of this.confirmed) if (!held.has(path) && disk?.text !== void 0 &&
|
|
1989
|
+
for (const [path, text] of this.cloud) if (typeof text === "string" && this.syncs(path)) held.set(path, text);
|
|
1990
|
+
for (const [path, disk] of this.confirmed) if (!held.has(path) && disk?.text !== void 0 && this.syncs(path)) held.set(path, disk.text);
|
|
1798
1991
|
for (const path of await this.walk()) {
|
|
1799
1992
|
if (held.has(path)) continue;
|
|
1800
1993
|
const local = await this.readLocal(path);
|
|
1801
|
-
if (local.kind === "text") held.set(path, local.text);
|
|
1994
|
+
if (local.kind === "text" && !exceedsFileLimit(local.text)) held.set(path, local.text);
|
|
1802
1995
|
}
|
|
1803
1996
|
return held;
|
|
1804
1997
|
}
|
|
@@ -1814,9 +2007,11 @@ var SyncClient = class extends EventEmitter {
|
|
|
1814
2007
|
return hash;
|
|
1815
2008
|
}
|
|
1816
2009
|
/**
|
|
1817
|
-
* A server without the hub gets the local one
|
|
1818
|
-
* already hold what the outbox's operations did, which the server does when they are sent, so a file
|
|
1819
|
-
* changed is uploaded as it was before the first one that did (d017).
|
|
2010
|
+
* A server without the hub gets the local one: what the server answered, or null when there is nothing here to upload.
|
|
2011
|
+
* The local files already hold what the outbox's operations did, which the server does when they are sent, so a file
|
|
2012
|
+
* an operation changed is uploaded as it was before the first one that did (d017). A file the client does not sync,
|
|
2013
|
+
* or larger than the server keeps, stays out, so the server takes the rest. A refusal before the hub, such as a 400,
|
|
2014
|
+
* is answered as a refusal; the server out of reach throws (Unreachable).
|
|
1820
2015
|
*/
|
|
1821
2016
|
async uploadIfEmpty() {
|
|
1822
2017
|
const files = {};
|
|
@@ -1826,23 +2021,46 @@ var SyncClient = class extends EventEmitter {
|
|
|
1826
2021
|
for (const [path, before] of Object.entries(entry.before ?? {})) if (!outbox.has(path)) outbox.set(path, before);
|
|
1827
2022
|
}
|
|
1828
2023
|
for (const path of /* @__PURE__ */ new Set([...await this.walk(), ...outbox.keys()])) {
|
|
2024
|
+
if (!this.syncs(path)) continue;
|
|
1829
2025
|
if (outbox.has(path)) {
|
|
1830
2026
|
const before = outbox.get(path);
|
|
1831
|
-
if (typeof before === "string") files[path] = before;
|
|
2027
|
+
if (typeof before === "string" && !exceedsFileLimit(before)) files[path] = before;
|
|
1832
2028
|
continue;
|
|
1833
2029
|
}
|
|
1834
2030
|
const local = await this.readLocal(path);
|
|
1835
|
-
if (local.kind === "text") files[path] = local.text;
|
|
2031
|
+
if (local.kind === "text" && !exceedsFileLimit(local.text)) files[path] = local.text;
|
|
1836
2032
|
}
|
|
1837
|
-
if (Object.keys(files).length === 0) return
|
|
2033
|
+
if (Object.keys(files).length === 0) return null;
|
|
1838
2034
|
const dirs = await this.walkDirs();
|
|
1839
|
-
|
|
2035
|
+
let result;
|
|
2036
|
+
try {
|
|
2037
|
+
result = await this.api.upload(files, dirs);
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
if (!(error instanceof ServerError)) throw error;
|
|
2040
|
+
result = {
|
|
2041
|
+
ok: false,
|
|
2042
|
+
message: error.message
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
1840
2045
|
if (!result.ok) {
|
|
1841
2046
|
this.log(`upload refused: ${result.message}`);
|
|
1842
|
-
return
|
|
2047
|
+
return {
|
|
2048
|
+
ok: false,
|
|
2049
|
+
message: result.message
|
|
2050
|
+
};
|
|
1843
2051
|
}
|
|
1844
2052
|
this.log(`uploaded the hub: ${result.files} files`);
|
|
1845
|
-
return true;
|
|
2053
|
+
return { ok: true };
|
|
2054
|
+
}
|
|
2055
|
+
/** Tell the writer, once for each reason, that the server has no copy of the hub and refused this one (snapshotNow). */
|
|
2056
|
+
refusedUpload(message) {
|
|
2057
|
+
if (this.uploadRefusal === message) return;
|
|
2058
|
+
this.uploadRefusal = message;
|
|
2059
|
+
this.problem({
|
|
2060
|
+
kind: "upload-refused",
|
|
2061
|
+
path: "",
|
|
2062
|
+
message: `the sync server has no copy of hub ${this.opts.settings.hub} and refused the upload of this environment's copy: ${message}. Nothing was taken in, removed or sent here; fix what the refusal names, and the next connection uploads the hub again`
|
|
2063
|
+
});
|
|
1846
2064
|
}
|
|
1847
2065
|
/** Reconcile `path` after whatever earlier work on it has finished; nothing once stopping. */
|
|
1848
2066
|
schedule(path) {
|
|
@@ -1893,6 +2111,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1893
2111
|
* each write an entry of its own; the server's version waits while the outbox has a write about the file (I4).
|
|
1894
2112
|
*/
|
|
1895
2113
|
async reconcile(path) {
|
|
2114
|
+
if (!this.syncs(path)) return;
|
|
1896
2115
|
if (this.awaiting.has(path)) return;
|
|
1897
2116
|
if (this.remote.has(path)) return this.reconcileRemote(path);
|
|
1898
2117
|
const local = await this.readLocal(path);
|
|
@@ -1902,6 +2121,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1902
2121
|
if (again.kind !== "binary" || again.hash !== local.hash) return this.schedule(path);
|
|
1903
2122
|
return this.skipBinary(path, local.hash);
|
|
1904
2123
|
}
|
|
2124
|
+
if (local.kind === "text" && exceedsFileLimit(local.text)) return this.skipLarge(path, local.text);
|
|
1905
2125
|
const seen = this.expected.get(path);
|
|
1906
2126
|
if (seen === void 0 ? local.kind !== "absent" : !this.matches(seen, local)) {
|
|
1907
2127
|
await this.whenFree(() => this.syncQueue());
|
|
@@ -1910,15 +2130,16 @@ var SyncClient = class extends EventEmitter {
|
|
|
1910
2130
|
const after = this.expected.get(path);
|
|
1911
2131
|
if (after !== seen && after !== void 0 && this.matches(after, local)) return this.schedule(path);
|
|
1912
2132
|
}
|
|
1913
|
-
const target = this.cloud.has(path) ? this.cloud.get(path) : this.synced ? null : void 0;
|
|
2133
|
+
const target = this.cloud.has(path) ? this.cloud.get(path) : this.synced && !this.scopeStale ? null : void 0;
|
|
1914
2134
|
const disk = this.expected.get(path);
|
|
2135
|
+
const unheld = target === null && this.unheld.has(path);
|
|
1915
2136
|
if (disk === void 0) {
|
|
1916
2137
|
if (local.kind === "absent") {
|
|
1917
2138
|
if (typeof target === "string") await this.replace(path, target);
|
|
1918
2139
|
return;
|
|
1919
2140
|
}
|
|
1920
2141
|
if (target === local.text) return this.adopt(path, local.text);
|
|
1921
|
-
if (!this.synced) return;
|
|
2142
|
+
if (!this.synced || target === void 0) return;
|
|
1922
2143
|
const answer = this.snapshotAnswers.get(path);
|
|
1923
2144
|
if (answer?.text === local.text && !this.pending(path)) {
|
|
1924
2145
|
this.snapshotAnswers.delete(path);
|
|
@@ -1949,7 +2170,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1949
2170
|
const again = await this.readLocal(path);
|
|
1950
2171
|
if (again.kind !== "text" || again.text !== "") return this.schedule(path);
|
|
1951
2172
|
}
|
|
1952
|
-
return this.queueLocal(path, local.text);
|
|
2173
|
+
return this.queueLocal(path, local.text, unheld ? { start: null } : {});
|
|
1953
2174
|
}
|
|
1954
2175
|
if (target === null || disk === null) {
|
|
1955
2176
|
this.forget(path);
|
|
@@ -1968,6 +2189,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1968
2189
|
return this.release(path);
|
|
1969
2190
|
}
|
|
1970
2191
|
if (this.pending(path)) return;
|
|
2192
|
+
if (unheld && local.kind === "text") return this.queueUnheld(path, local.text);
|
|
1971
2193
|
await this.replace(path, target);
|
|
1972
2194
|
}
|
|
1973
2195
|
/**
|
|
@@ -2039,15 +2261,25 @@ var SyncClient = class extends EventEmitter {
|
|
|
2039
2261
|
this.history.set(path, list);
|
|
2040
2262
|
}
|
|
2041
2263
|
skipBinary(path, hash) {
|
|
2042
|
-
const key =
|
|
2043
|
-
if (this.
|
|
2044
|
-
this.
|
|
2264
|
+
const key = `binary:${path}:${hash}`;
|
|
2265
|
+
if (this.skipped.has(key)) return;
|
|
2266
|
+
this.skipped.add(key);
|
|
2045
2267
|
this.problem({
|
|
2046
2268
|
kind: "skipped-binary",
|
|
2047
2269
|
path,
|
|
2048
2270
|
message: `${path} is not UTF-8 text, so it is not synced and not overwritten; other environments do not see it`
|
|
2049
2271
|
});
|
|
2050
2272
|
}
|
|
2273
|
+
skipLarge(path, text) {
|
|
2274
|
+
const key = `large:${path}:${contentHash(text)}`;
|
|
2275
|
+
if (this.skipped.has(key)) return;
|
|
2276
|
+
this.skipped.add(key);
|
|
2277
|
+
this.problem({
|
|
2278
|
+
kind: "skipped-large",
|
|
2279
|
+
path,
|
|
2280
|
+
message: `${path} is larger than the ${MAX_FILE_BYTES} bytes the sync server keeps of a file, so it is not synced and not overwritten; other environments do not see it. Split it, or add it to .mainflow/.gitignore`
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2051
2283
|
/**
|
|
2052
2284
|
* Whether mainline `dir` is open, closed (done or dropped) or of a status the client does not know, by its
|
|
2053
2285
|
* mainline.md: the server's text the client took in, else the file here.
|
|
@@ -2157,7 +2389,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2157
2389
|
* and `have` does not name it. Returns, besides, the files of other paths listed that may not stay on the server.
|
|
2158
2390
|
*/
|
|
2159
2391
|
async fetchNow(targets) {
|
|
2160
|
-
const sorted = [...new Set(targets)].filter(
|
|
2392
|
+
const sorted = [...new Set(targets)].filter((path) => this.syncs(path)).sort(comparePaths);
|
|
2161
2393
|
const again = /* @__PURE__ */ new Set();
|
|
2162
2394
|
if (sorted.length === 0 || this.closing) return {
|
|
2163
2395
|
fetched: [],
|
|
@@ -2169,7 +2401,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2169
2401
|
let done = false;
|
|
2170
2402
|
try {
|
|
2171
2403
|
const have = /* @__PURE__ */ new Map();
|
|
2172
|
-
for (const [path, text] of this.cloud) if (typeof text === "string" && !want.has(path) &&
|
|
2404
|
+
for (const [path, text] of this.cloud) if (typeof text === "string" && !want.has(path) && this.syncs(path)) have.set(path, this.hashOf(path, text));
|
|
2173
2405
|
for (const [path, hash] of this.remote) if (!want.has(path)) have.set(path, hash);
|
|
2174
2406
|
const listOnly = this.listOnly(false)?.filter((prefix) => !sorted.some((path) => path.startsWith(prefix)));
|
|
2175
2407
|
const first = sorted[0];
|
|
@@ -2192,6 +2424,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2192
2424
|
continue;
|
|
2193
2425
|
}
|
|
2194
2426
|
mentioned.add(event.path);
|
|
2427
|
+
if (!this.syncs(event.path)) continue;
|
|
2195
2428
|
if (event.kind === "listed") {
|
|
2196
2429
|
if (want.has(event.path)) continue;
|
|
2197
2430
|
if (this.mayStay(event.path)) this.keepRemote(event.path, event.hash, part.seq);
|
|
@@ -2307,7 +2540,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2307
2540
|
await this.fetchPaths([path]).catch((error) => this.log(`${path}: could not download it: ${error.message}`));
|
|
2308
2541
|
}
|
|
2309
2542
|
restoreRemote() {
|
|
2310
|
-
for (const [path, hash] of Object.entries(readRemote(this.root))) if (
|
|
2543
|
+
for (const [path, hash] of Object.entries(readRemote(this.root))) if (this.syncs(path) && artifactOf(path) !== null && this.expected.get(path) === void 0) this.remote.set(path, hash);
|
|
2311
2544
|
}
|
|
2312
2545
|
saveRemote() {
|
|
2313
2546
|
if (this.remoteTimer) return;
|
|
@@ -2628,6 +2861,24 @@ var SyncClient = class extends EventEmitter {
|
|
|
2628
2861
|
this.trace(`local ${path} ${short(text)} changed before it was queued; reconciling it again`);
|
|
2629
2862
|
this.schedule(path);
|
|
2630
2863
|
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Queue `path`, which holds `text` as the client left it there, as a new file: the server never held a file at it
|
|
2866
|
+
* (`unheld`). Under the write lock that queues it, the file must still hold it and the server still have no text of
|
|
2867
|
+
* it; otherwise it is reconciled again.
|
|
2868
|
+
*/
|
|
2869
|
+
async queueUnheld(path, text) {
|
|
2870
|
+
if (await this.enqueue(() => {
|
|
2871
|
+
if (this.expected.get(path)?.hash !== contentHash(text) || !this.onDisk(path, text) || !this.unheld.has(path) || typeof this.cloud.get(path) === "string") return null;
|
|
2872
|
+
return {
|
|
2873
|
+
kind: "patch",
|
|
2874
|
+
path,
|
|
2875
|
+
text,
|
|
2876
|
+
base: null
|
|
2877
|
+
};
|
|
2878
|
+
}) !== null) return;
|
|
2879
|
+
this.trace(`new ${path} changed before it was queued; reconciling it again`);
|
|
2880
|
+
this.schedule(path);
|
|
2881
|
+
}
|
|
2631
2882
|
/** What the response to queued edit `entry` means for the writer (layer 2), then wait for the broadcast it caused. */
|
|
2632
2883
|
async settleEdit(entry, response, context, sent) {
|
|
2633
2884
|
const { path } = entry;
|
|
@@ -3397,7 +3648,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
3397
3648
|
...nodeStorage(this.root),
|
|
3398
3649
|
lock: (fn) => fn()
|
|
3399
3650
|
};
|
|
3400
|
-
const seen = new Map([...new Set(paths)].filter(
|
|
3651
|
+
const seen = new Map([...new Set(paths)].filter((path) => this.syncs(path)).map((path) => [path, this.expected.get(path)]));
|
|
3401
3652
|
const changes = renumberFiles(storage, seen.keys(), renumbered);
|
|
3402
3653
|
const unknown = /* @__PURE__ */ new Set();
|
|
3403
3654
|
for (const [path, disk] of seen) {
|
|
@@ -3450,7 +3701,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
3450
3701
|
*/
|
|
3451
3702
|
async flushEdit(edit, timeoutMs = 2e4) {
|
|
3452
3703
|
const { path } = edit;
|
|
3453
|
-
if (this.caughtUp &&
|
|
3704
|
+
if (this.caughtUp && this.syncs(path) && edit.old !== "") await this.withChain(path, () => this.replayEdit(edit));
|
|
3454
3705
|
return this.flush(timeoutMs);
|
|
3455
3706
|
}
|
|
3456
3707
|
/**
|
|
@@ -3513,28 +3764,61 @@ var SyncClient = class extends EventEmitter {
|
|
|
3513
3764
|
}]);
|
|
3514
3765
|
if ("files" in response && response.files.some((f) => f.path === path)) await this.delivery(path, response.seq);
|
|
3515
3766
|
}
|
|
3516
|
-
|
|
3767
|
+
/**
|
|
3768
|
+
* The synced files here, by path. Each directory's .gitignore is read again on the way, before what the directory
|
|
3769
|
+
* holds, so a change to one takes effect from the next scan on; a directory the rules leave out is not walked into.
|
|
3770
|
+
* Rules changed since the last snapshot was asked for take it again: a file they let in that is on the server, as
|
|
3771
|
+
* another environment sent it while the rules here left it out, comes then, not only on the next connection. A walk
|
|
3772
|
+
* of a snapshot being taken leaves that to it (resync).
|
|
3773
|
+
*/
|
|
3774
|
+
async walk() {
|
|
3517
3775
|
const out = [];
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
entries
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3776
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
3777
|
+
const visit = async (dir) => {
|
|
3778
|
+
let entries;
|
|
3779
|
+
try {
|
|
3780
|
+
entries = await readdir(join(this.paths.data, dir), { withFileTypes: true });
|
|
3781
|
+
} catch {
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3784
|
+
dirs.add(dir);
|
|
3785
|
+
this.scope.set(dir, entries.some((entry) => entry.name === ".gitignore" && entry.isFile()) ? await this.readRules(dir) : null);
|
|
3786
|
+
for (const entry of entries) {
|
|
3787
|
+
const path = dir ? `${dir}/${entry.name}` : entry.name;
|
|
3788
|
+
if (entry.isDirectory()) {
|
|
3789
|
+
if (this.scope.walks(path)) await visit(path);
|
|
3790
|
+
} else if (entry.isFile() && this.syncs(path)) out.push(path);
|
|
3791
|
+
}
|
|
3792
|
+
};
|
|
3793
|
+
await visit("");
|
|
3794
|
+
this.scope.keepOnly(dirs);
|
|
3795
|
+
if (this.scopeStale && this.synced && this.connected && this.resyncing === null && !this.closing) {
|
|
3796
|
+
this.log("the hub's .gitignore changed; reloading the snapshot for the files it lets in");
|
|
3797
|
+
this.track(this.resync().catch((error) => this.log(`could not reload the snapshot: ${error.message}`)));
|
|
3529
3798
|
}
|
|
3530
3799
|
return out;
|
|
3531
3800
|
}
|
|
3801
|
+
/**
|
|
3802
|
+
* The text of the .gitignore in `dir`, which is there, as walk takes it. A text other than the rules have may be one
|
|
3803
|
+
* being written, as `cat >` empties a file before it fills it, and a rule missing for a moment would let its files in,
|
|
3804
|
+
* to be sent: it is read again after EMPTY_RECHECK_MS and taken only when it reads the same, empty too. Until then, and
|
|
3805
|
+
* while the file cannot be read, the rules stay as they were; the next walk reads it again.
|
|
3806
|
+
*/
|
|
3807
|
+
async readRules(dir) {
|
|
3808
|
+
const held = this.scope.text(dir);
|
|
3809
|
+
const read = () => readFile(join(this.paths.data, dir, GITIGNORE), "utf8").catch(() => null);
|
|
3810
|
+
const text = await read();
|
|
3811
|
+
if (text === null || text === held) return held;
|
|
3812
|
+
await sleep$1(EMPTY_RECHECK_MS);
|
|
3813
|
+
return await read() === text ? text : held;
|
|
3814
|
+
}
|
|
3815
|
+
/** The directories an upload holds: every one outside local/ that the hub's .gitignore files leave in. */
|
|
3532
3816
|
async walkDirs(dir = "") {
|
|
3533
3817
|
const out = [];
|
|
3534
3818
|
const entries = await readdir(join(this.paths.data, dir), { withFileTypes: true }).catch(() => []);
|
|
3535
3819
|
for (const entry of entries) {
|
|
3536
3820
|
const path = dir ? `${dir}/${entry.name}` : entry.name;
|
|
3537
|
-
if (!entry.isDirectory() || path === "local" || path.startsWith("local/")) continue;
|
|
3821
|
+
if (!entry.isDirectory() || path === "local" || path.startsWith("local/") || this.scope.ignored(path, true)) continue;
|
|
3538
3822
|
out.push(path, ...await this.walkDirs(path));
|
|
3539
3823
|
}
|
|
3540
3824
|
return out;
|
|
@@ -3557,7 +3841,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
3557
3841
|
if (!baseline) this.schedule(path);
|
|
3558
3842
|
}
|
|
3559
3843
|
if (baseline) return;
|
|
3560
|
-
for (const [path, disk] of this.expected) if (disk !== null && !seen.has(path)) {
|
|
3844
|
+
for (const [path, disk] of this.expected) if (disk !== null && !seen.has(path) && this.syncs(path)) {
|
|
3561
3845
|
this.signatures.delete(path);
|
|
3562
3846
|
this.schedule(path);
|
|
3563
3847
|
}
|
|
@@ -3578,12 +3862,13 @@ var SyncClient = class extends EventEmitter {
|
|
|
3578
3862
|
}, SETTLE_MS));
|
|
3579
3863
|
return;
|
|
3580
3864
|
}
|
|
3581
|
-
|
|
3865
|
+
const rules = path === ".gitignore" || path.endsWith(`/.gitignore`);
|
|
3866
|
+
if (!rules && !this.syncs(path)) return;
|
|
3582
3867
|
this.trace(`watched ${path}`);
|
|
3583
3868
|
clearTimeout(timers.get(path));
|
|
3584
3869
|
timers.set(path, setTimeout(() => {
|
|
3585
3870
|
timers.delete(path);
|
|
3586
|
-
this.track(this.whenSettled(path).then(() => this.schedule(path)));
|
|
3871
|
+
this.track(this.whenSettled(path).then(() => rules ? this.scan(true) : this.schedule(path)));
|
|
3587
3872
|
}, SETTLE_MS));
|
|
3588
3873
|
};
|
|
3589
3874
|
try {
|
|
@@ -4208,4 +4493,4 @@ function hubWriter(root, env = process.env, options = {}) {
|
|
|
4208
4493
|
};
|
|
4209
4494
|
}
|
|
4210
4495
|
//#endregion
|
|
4211
|
-
export {
|
|
4496
|
+
export { hubName as C, isSynced as D, SYNC_DIR as E, syncPaths as O, earlierToken as S, SETTINGS_FILE as T, readRemote as _, remoteMode as a, ServerError as b, planFetch as c, DaemonRunning as d, DaemonUnavailable as f, silentHint as g, readDaemon as h, queuedCount as i, planFlush as l, probeDaemon as m, hubWriter as n, fetchDenial as o, callDaemon as p, writtenLines as r, hookAnswer as s, CONFIRM_MS as t, startDaemon as u, oversizedFiles as v, syncSettings as w, ENV as x, ServerApi as y };
|