aethel 0.3.5 → 0.3.6
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/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/src/cli.js +16 -6
- package/src/core/config.js +2 -1
- package/src/core/remote-cache.js +1 -0
- package/src/core/repository.js +23 -10
- package/src/core/snapshot.js +16 -12
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -385,7 +385,7 @@ function handleReset(paths, options) {
|
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
async function handleCommit(options, { repo: existingRepo } = {}) {
|
|
388
|
+
async function handleCommit(options, { repo: existingRepo, snapshotHint } = {}) {
|
|
389
389
|
const repo = existingRepo || await openRepo(options);
|
|
390
390
|
const staged = repo.getStagedEntries();
|
|
391
391
|
|
|
@@ -409,7 +409,9 @@ async function handleCommit(options, { repo: existingRepo } = {}) {
|
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
const spinner = createSpinner("Saving snapshot...");
|
|
412
|
-
|
|
412
|
+
// snapshotHint lets callers (pull/push) pass pre-loaded state
|
|
413
|
+
// so saveSnapshot skips redundant API calls / fs scans.
|
|
414
|
+
await repo.saveSnapshot(message, snapshotHint);
|
|
413
415
|
spinner.succeed(`Snapshot saved: "${message}"`);
|
|
414
416
|
}
|
|
415
417
|
|
|
@@ -470,7 +472,7 @@ async function handleFetch(options) {
|
|
|
470
472
|
|
|
471
473
|
async function handlePull(paths, options) {
|
|
472
474
|
const repo = await openRepo(options);
|
|
473
|
-
const { diff } = await loadStateWithProgress(repo, { useCache: false });
|
|
475
|
+
const { diff, remoteState } = await loadStateWithProgress(repo, { useCache: false });
|
|
474
476
|
|
|
475
477
|
let remoteChanges = diff.changes.filter((change) =>
|
|
476
478
|
[
|
|
@@ -514,12 +516,16 @@ async function handlePull(paths, options) {
|
|
|
514
516
|
|
|
515
517
|
const count = repo.stageChanges(remoteChanges);
|
|
516
518
|
console.log(`Staged ${count} remote change(s). Committing...`);
|
|
517
|
-
|
|
519
|
+
// Pull downloads remote→local: remote state unchanged, only re-scan local
|
|
520
|
+
await handleCommit({ ...options, message: options.message || "pull" }, {
|
|
521
|
+
repo,
|
|
522
|
+
snapshotHint: { remote: remoteState },
|
|
523
|
+
});
|
|
518
524
|
}
|
|
519
525
|
|
|
520
526
|
async function handlePush(paths, options) {
|
|
521
527
|
const repo = await openRepo(options);
|
|
522
|
-
const { diff } = await loadStateWithProgress(repo, { useCache: false });
|
|
528
|
+
const { diff, local } = await loadStateWithProgress(repo, { useCache: false });
|
|
523
529
|
|
|
524
530
|
let localChanges = diff.changes.filter((change) =>
|
|
525
531
|
[
|
|
@@ -563,7 +569,11 @@ async function handlePush(paths, options) {
|
|
|
563
569
|
|
|
564
570
|
const count = repo.stageChanges(localChanges);
|
|
565
571
|
console.log(`Staged ${count} local change(s). Committing...`);
|
|
566
|
-
|
|
572
|
+
// Push uploads local→remote: local state unchanged, only re-fetch remote
|
|
573
|
+
await handleCommit({ ...options, message: options.message || "push" }, {
|
|
574
|
+
repo,
|
|
575
|
+
snapshotHint: { local },
|
|
576
|
+
});
|
|
567
577
|
}
|
|
568
578
|
|
|
569
579
|
async function handleResolve(paths, options) {
|
package/src/core/config.js
CHANGED
|
@@ -113,5 +113,6 @@ export function writeSnapshot(root, snapshot) {
|
|
|
113
113
|
fs.copyFileSync(latest, path.join(snapDir, HISTORY_DIR, `${ts}.json`));
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
// Compact JSON — snapshots can be large, pretty-printing is slow + wastes disk
|
|
117
|
+
fs.writeFileSync(latest, JSON.stringify(snapshot) + "\n");
|
|
117
118
|
}
|
package/src/core/remote-cache.js
CHANGED
|
@@ -45,6 +45,7 @@ export function readRemoteCache(root, rootFolderId = null, ttlMs = DEFAULT_TTL_M
|
|
|
45
45
|
|
|
46
46
|
export function writeRemoteCache(root, remoteState, rootFolderId = null) {
|
|
47
47
|
const p = cachePath(root);
|
|
48
|
+
// Compact JSON — cache can be large with many files
|
|
48
49
|
fs.writeFileSync(
|
|
49
50
|
p,
|
|
50
51
|
JSON.stringify({
|
package/src/core/repository.js
CHANGED
|
@@ -112,17 +112,19 @@ export class Repository {
|
|
|
112
112
|
async loadState({ useCache = true } = {}) {
|
|
113
113
|
const config = this.getConfig();
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
// Run all three in parallel — remote fetch is the slowest, overlap it
|
|
116
|
+
// with local scan and snapshot read.
|
|
117
|
+
const [local, snapshot, remoteState] = await Promise.all([
|
|
116
118
|
scanLocal(this._root),
|
|
117
119
|
Promise.resolve(readLatestSnapshot(this._root)),
|
|
120
|
+
this._loadRemoteState({ useCache }),
|
|
118
121
|
]);
|
|
119
|
-
|
|
120
|
-
const remoteState = await this._loadRemoteState({ useCache });
|
|
121
122
|
const remote = remoteState.files;
|
|
122
123
|
|
|
123
124
|
return {
|
|
124
125
|
config,
|
|
125
126
|
remote,
|
|
127
|
+
remoteState,
|
|
126
128
|
local,
|
|
127
129
|
snapshot,
|
|
128
130
|
diff: computeDiff(snapshot, remote, local, { root: this._root }),
|
|
@@ -178,20 +180,31 @@ export class Repository {
|
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
/**
|
|
181
|
-
*
|
|
183
|
+
* Build and persist a new snapshot.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} message
|
|
186
|
+
* @param {object} [preloaded]
|
|
187
|
+
* @param {object} [preloaded.remote] Reuse this remote state (skip API call)
|
|
188
|
+
* @param {object} [preloaded.local] Reuse this local scan (skip fs walk)
|
|
182
189
|
*/
|
|
183
|
-
async saveSnapshot(message = "sync") {
|
|
190
|
+
async saveSnapshot(message = "sync", { remote, local } = {}) {
|
|
184
191
|
const config = this.getConfig();
|
|
185
192
|
const rootFolderId = config.drive_folder_id || null;
|
|
186
193
|
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
// Only fetch what wasn't pre-loaded, in parallel.
|
|
195
|
+
const needRemote = !remote;
|
|
196
|
+
const needLocal = !local;
|
|
197
|
+
|
|
198
|
+
if (needRemote) invalidateRemoteCache(this._root);
|
|
199
|
+
|
|
200
|
+
const [remoteState, localFiles] = await Promise.all([
|
|
201
|
+
needRemote ? getRemoteState(this.drive, rootFolderId) : remote,
|
|
202
|
+
needLocal ? scanLocal(this._root) : local,
|
|
191
203
|
]);
|
|
204
|
+
|
|
192
205
|
assertNoDuplicateFolders(remoteState.duplicateFolders);
|
|
193
206
|
writeRemoteCache(this._root, remoteState, rootFolderId);
|
|
194
|
-
writeSnapshot(this._root, buildSnapshot(remoteState.files,
|
|
207
|
+
writeSnapshot(this._root, buildSnapshot(remoteState.files, localFiles, message));
|
|
195
208
|
}
|
|
196
209
|
|
|
197
210
|
// ── Cache management ────────────────────────────────────────────────
|
package/src/core/snapshot.js
CHANGED
|
@@ -42,7 +42,7 @@ function saveHashCache(root, cache) {
|
|
|
42
42
|
|
|
43
43
|
// ── Scanning ─────────────────────────────────────────────────────────
|
|
44
44
|
|
|
45
|
-
const PARALLEL_HASH_LIMIT =
|
|
45
|
+
const PARALLEL_HASH_LIMIT = 128;
|
|
46
46
|
|
|
47
47
|
export async function scanLocal(root, { respectIgnore = true } = {}) {
|
|
48
48
|
const resolvedRoot = path.resolve(root);
|
|
@@ -54,7 +54,8 @@ export async function scanLocal(root, { respectIgnore = true } = {}) {
|
|
|
54
54
|
const filesToHash = [];
|
|
55
55
|
// Track directories and their child counts to detect empty folders
|
|
56
56
|
const dirChildCount = new Map();
|
|
57
|
-
|
|
57
|
+
// Map relative dir path → absolute path (for deferred stat on empty dirs only)
|
|
58
|
+
const dirAbsPath = new Map();
|
|
58
59
|
|
|
59
60
|
async function walk(currentPath) {
|
|
60
61
|
let entries;
|
|
@@ -72,6 +73,7 @@ export async function scanLocal(root, { respectIgnore = true } = {}) {
|
|
|
72
73
|
if (relativeDirPath !== null) {
|
|
73
74
|
if (!dirChildCount.has(relativeDirPath)) {
|
|
74
75
|
dirChildCount.set(relativeDirPath, 0);
|
|
76
|
+
dirAbsPath.set(relativeDirPath, currentPath);
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -110,12 +112,6 @@ export async function scanLocal(root, { respectIgnore = true } = {}) {
|
|
|
110
112
|
|
|
111
113
|
if (relativeDirPath !== null) {
|
|
112
114
|
dirChildCount.set(relativeDirPath, trackedChildren);
|
|
113
|
-
try {
|
|
114
|
-
const stat = await fs.promises.stat(currentPath);
|
|
115
|
-
dirStats.set(relativeDirPath, stat);
|
|
116
|
-
} catch {
|
|
117
|
-
// ignore
|
|
118
|
-
}
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
await Promise.all([
|
|
@@ -171,16 +167,24 @@ export async function scanLocal(root, { respectIgnore = true } = {}) {
|
|
|
171
167
|
}
|
|
172
168
|
}
|
|
173
169
|
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
// Only stat the empty directories (not all directories)
|
|
171
|
+
await Promise.all([...emptyDirs].map(async (dirPath) => {
|
|
172
|
+
let mtime = new Date().toISOString();
|
|
173
|
+
const absPath = dirAbsPath.get(dirPath);
|
|
174
|
+
if (absPath) {
|
|
175
|
+
try {
|
|
176
|
+
const stat = await fs.promises.stat(absPath);
|
|
177
|
+
mtime = new Date(stat.mtimeMs).toISOString();
|
|
178
|
+
} catch { /* ignore */ }
|
|
179
|
+
}
|
|
176
180
|
result[dirPath] = {
|
|
177
181
|
localPath: dirPath,
|
|
178
182
|
isFolder: true,
|
|
179
183
|
size: 0,
|
|
180
184
|
md5: null,
|
|
181
|
-
modifiedTime:
|
|
185
|
+
modifiedTime: mtime,
|
|
182
186
|
};
|
|
183
|
-
}
|
|
187
|
+
}));
|
|
184
188
|
|
|
185
189
|
// Persist updated cache
|
|
186
190
|
saveHashCache(resolvedRoot, nextCache);
|