sliftutils 1.7.105 → 1.7.107

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.
Files changed (54) hide show
  1. package/dist/treeSummary.ts.cache +2 -2
  2. package/index.d.ts +7 -4
  3. package/misc/dist/environment.ts.cache +2 -2
  4. package/misc/dist/getSecret.ts.cache +2 -2
  5. package/misc/dist/strings.ts.cache +2 -2
  6. package/misc/dist/zip.ts.cache +2 -2
  7. package/misc/https/dist/certs.ts.cache +2 -2
  8. package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
  9. package/package.json +1 -1
  10. package/render-utils/dist/observer.tsx.cache +2 -2
  11. package/storage/ArchivesDisk.ts +1 -1
  12. package/storage/dist/ArchivesDisk.ts.cache +3 -3
  13. package/storage/dist/FileFolderAPI.tsx.cache +2 -2
  14. package/storage/dist/IArchives.ts.cache +2 -2
  15. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
  16. package/storage/dist/JSONStorage.ts.cache +2 -2
  17. package/storage/dist/PendingManager.tsx.cache +2 -2
  18. package/storage/dist/StreamingLogs.ts.cache +2 -2
  19. package/storage/dist/TransactionFile.ts.cache +2 -2
  20. package/storage/dist/TransactionStorage.ts.cache +2 -2
  21. package/storage/dist/archiveHelpers.ts.cache +2 -2
  22. package/storage/dist/backblaze.ts.cache +2 -2
  23. package/storage/dist/fileSystemPointer.ts.cache +2 -2
  24. package/storage/dist/remoteFileStorage.ts.cache +2 -2
  25. package/storage/remoteStorage/dist/ArchivesDelayed.ts.cache +2 -2
  26. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +2 -2
  27. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +2 -2
  28. package/storage/remoteStorage/dist/accessStats.ts.cache +2 -2
  29. package/storage/remoteStorage/dist/blobStore.ts.cache +2 -2
  30. package/storage/remoteStorage/dist/bucketDisk.ts.cache +2 -2
  31. package/storage/remoteStorage/dist/certTrustModal.tsx.cache +2 -2
  32. package/storage/remoteStorage/dist/chainStartup.ts.cache +2 -2
  33. package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
  34. package/storage/remoteStorage/dist/createArchives.ts.cache +2 -2
  35. package/storage/remoteStorage/dist/deployTakeover.ts.cache +2 -2
  36. package/storage/remoteStorage/dist/intermediateManagement.ts.cache +2 -2
  37. package/storage/remoteStorage/dist/intermediateSources.ts.cache +2 -2
  38. package/storage/remoteStorage/dist/remoteConfig.ts.cache +2 -2
  39. package/storage/remoteStorage/dist/serverConfig.ts.cache +2 -2
  40. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +2 -2
  41. package/storage/remoteStorage/dist/sourcesList.ts.cache +2 -2
  42. package/storage/remoteStorage/dist/storageClientController.ts.cache +2 -2
  43. package/storage/remoteStorage/dist/storageController.ts.cache +2 -2
  44. package/storage/remoteStorage/dist/storageLogs.ts.cache +61 -42
  45. package/storage/remoteStorage/dist/storageServerState.ts.cache +2 -2
  46. package/storage/remoteStorage/dist/storeConfig.ts.cache +2 -2
  47. package/storage/remoteStorage/dist/storePlan.ts.cache +2 -2
  48. package/storage/remoteStorage/dist/storeSources.ts.cache +2 -2
  49. package/storage/remoteStorage/dist/storeSync.ts.cache +72 -61
  50. package/storage/remoteStorage/dist/validation.ts.cache +2 -2
  51. package/storage/remoteStorage/storageLogs.d.ts +3 -2
  52. package/storage/remoteStorage/storageLogs.ts +56 -35
  53. package/storage/remoteStorage/storeSync.d.ts +4 -2
  54. package/storage/remoteStorage/storeSync.ts +69 -56
@@ -1,4 +1,4 @@
1
- "use strict"; var console = (function shimConsole() { let baseConsole = globalThis.console; if (typeof window !== "undefined") return baseConsole; let name = __filename.replaceAll("\\", "/").split("/").pop(); let dirName = __dirname.replaceAll("\\", "/").split("/").pop(); let context = { __FILE__: __filename, __DIR__: __dirname, __NAME__: name, __DIRNAME__: dirName }; let newConsole = { ...baseConsole }; for (let shim of ["log", "info", "warn", "error"]) { newConsole[shim] = (...args) => { args.push(context); baseConsole[shim](...args); }; } newConsole.__context = context; return newConsole; })(); "use strict";
1
+ "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
3
3
  //exports.assertValidArgs = exports.assertValidPath = exports.assertValidSourceName = exports.assertValidName = void 0;
4
4
  function assertValidName(value, kind) {
@@ -47,4 +47,4 @@ function assertValidArgs(target, key, descriptor) {
47
47
  }
48
48
  exports.assertValidArgs = assertValidArgs;
49
49
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInZhbGlkYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsU0FBZ0IsZUFBZSxDQUFDLEtBQWEsRUFBRSxJQUFZO0lBQ3ZELElBQUksQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDL0IsTUFBTSxJQUFJLEtBQUssQ0FBQyxXQUFXLElBQUksSUFBSSxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQywrREFBK0QsQ0FBQyxDQUFDO0lBQzdILENBQUM7QUFDTCxDQUFDO0FBSkQsMENBSUM7QUFFRCx1VEFBdVQ7QUFDdlQsU0FBZ0IscUJBQXFCLENBQUMsS0FBYTtJQUMvQyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDaEMsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsbUVBQW1FLENBQUMsQ0FBQztJQUNySSxDQUFDO0lBQ0QsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDdEIsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsb0VBQW9FLENBQUMsQ0FBQztJQUN0SSxDQUFDO0FBQ0wsQ0FBQztBQVBELHNEQU9DO0FBRUQsU0FBZ0IsZUFBZSxDQUFDLElBQVk7SUFDeEMsSUFBSSxNQUFNLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxNQUFNLENBQUMsQ0FBQyxNQUFNLEdBQUcsSUFBSSxFQUFFLENBQUM7UUFDMUMsTUFBTSxJQUFJLEtBQUssQ0FBQyxrQkFBa0IsSUFBSSxDQUFDLE1BQU0sNkJBQTZCLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUNwRyxDQUFDO0lBQ0QsSUFBSSxDQUFDLElBQUksSUFBSSxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUM3SCxNQUFNLElBQUksS0FBSyxDQUFDLGdCQUFnQixJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDLDhGQUE4RixDQUFDLENBQUM7SUFDdEssQ0FBQztJQUNELElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLEtBQUssR0FBRyxJQUFJLElBQUksS0FBSyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQzlELE1BQU0sSUFBSSxLQUFLLENBQUMsZ0JBQWdCLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMseUNBQXlDLENBQUMsQ0FBQztJQUNqSCxDQUFDO0FBQ0wsQ0FBQztBQVZELDBDQVVDO0FBRUQsNFdBQTRXO0FBQzVXLFNBQWdCLGVBQWUsQ0FBQyxNQUFlLEVBQUUsR0FBVyxFQUFFLFVBQThCO0lBQ3hGLElBQUksUUFBUSxHQUFHLFVBQVUsQ0FBQyxLQUF3QyxDQUFDO0lBQ25FLFVBQVUsQ0FBQyxLQUFLLEdBQUcsVUFBVSxHQUFHLElBQWU7UUFDM0MsSUFBSSxNQUFNLEdBQUcsSUFBSSxDQUFDLENBQUMsQ0FBeUUsQ0FBQztRQUM3RixJQUFJLE1BQU0sSUFBSSxPQUFPLE1BQU0sS0FBSyxRQUFRLEVBQUUsQ0FBQztZQUN2QyxJQUFJLFNBQVMsSUFBSSxNQUFNO2dCQUFFLGVBQWUsQ0FBQyxNQUFNLENBQUMsT0FBaUIsRUFBRSxTQUFTLENBQUMsQ0FBQztZQUM5RSxJQUFJLFlBQVksSUFBSSxNQUFNO2dCQUFFLGVBQWUsQ0FBQyxNQUFNLENBQUMsVUFBb0IsRUFBRSxhQUFhLENBQUMsQ0FBQztZQUN4RixJQUFJLE1BQU0sSUFBSSxNQUFNO2dCQUFFLGVBQWUsQ0FBQyxNQUFNLENBQUMsSUFBYyxDQUFDLENBQUM7UUFDakUsQ0FBQztRQUNELE9BQU8sUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUM7SUFDdEMsQ0FBQyxDQUFDO0FBQ04sQ0FBQztBQVhELDBDQVdDIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIGFzc2VydFZhbGlkTmFtZSh2YWx1ZTogc3RyaW5nLCBraW5kOiBzdHJpbmcpOiB2b2lkIHtcbiAgICBpZiAoIS9eW1xcdy1dezEsNjR9JC8udGVzdCh2YWx1ZSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBJbnZhbGlkICR7a2luZH0gJHtKU09OLnN0cmluZ2lmeSh2YWx1ZSl9LCBleHBlY3RlZCAxLTY0IGNoYXJhY3RlcnMgb2YgbGV0dGVycy9udW1iZXJzL3VuZGVyc2NvcmUvZGFzaGApO1xuICAgIH1cbn1cblxuLyoqIEEgc3RvcmUncyBuYW1lIChzZWUgQ29tbW9uQ29uZmlnLm5hbWUpLCB3aGljaCBhbHNvIGFsbG93cyBkb3RzIC0gYSBuYW1lIGlzIG9mdGVuIGEgaG9zdCBvciBhIHZlcnNpb24sIGFuZCBib3RoIHJlYWQgd3Jvbmcgd2l0aG91dCB0aGVtLiBJdCBpcyBvbmUgcGF0aCBzZWdtZW50IG9mIHRoZSBzdG9yZSdzIGZvbGRlciwgc28gdGhlIHR3byBuYW1lcyB0aGF0IHdvdWxkIG1lYW4gYSBkaWZmZXJlbnQgZm9sZGVyIGVudGlyZWx5IGFyZSByZWplY3RlZDogZXZlcnl0aGluZyBlbHNlIGNvbnRhaW5pbmcgZG90cyBpcyBqdXN0IGEgbmFtZS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnRWYWxpZFNvdXJjZU5hbWUodmFsdWU6IHN0cmluZyk6IHZvaWQge1xuICAgIGlmICghL15bXFx3Li1dezEsNjR9JC8udGVzdCh2YWx1ZSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBJbnZhbGlkIHNvdXJjZSBuYW1lICR7SlNPTi5zdHJpbmdpZnkodmFsdWUpfSwgZXhwZWN0ZWQgMS02NCBjaGFyYWN0ZXJzIG9mIGxldHRlcnMvbnVtYmVycy91bmRlcnNjb3JlL2Rhc2gvZG90YCk7XG4gICAgfVxuICAgIGlmICgvXlxcLiskLy50ZXN0KHZhbHVlKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgc291cmNlIG5hbWUgJHtKU09OLnN0cmluZ2lmeSh2YWx1ZSl9OiBhIG5hbWUgb2Ygb25seSBkb3RzIHJlZmVycyB0byBhIGRpcmVjdG9yeSByYXRoZXIgdGhhbiBuYW1pbmcgb25lYCk7XG4gICAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gYXNzZXJ0VmFsaWRQYXRoKHBhdGg6IHN0cmluZyk6IHZvaWQge1xuICAgIGlmIChCdWZmZXIuZnJvbShwYXRoLCBcInV0ZjhcIikubGVuZ3RoID4gMTAwMCkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFBhdGggdG9vIGxvbmc6ICR7cGF0aC5sZW5ndGh9IGNoYXJhY3RlcnMgPiAxMDAwLiBQYXRoOiAke3BhdGguc2xpY2UoMCwgMjAwKX1gKTtcbiAgICB9XG4gICAgaWYgKCFwYXRoIHx8IHBhdGguc3RhcnRzV2l0aChcIi9cIikgfHwgcGF0aC5lbmRzV2l0aChcIi9cIikgfHwgcGF0aC5pbmNsdWRlcyhcIi8vXCIpIHx8IHBhdGguaW5jbHVkZXMoXCJcXFxcXCIpIHx8IHBhdGguaW5jbHVkZXMoXCJcXHgwMFwiKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgcGF0aCAke0pTT04uc3RyaW5naWZ5KHBhdGguc2xpY2UoMCwgMjAwKSl9LCBwYXRocyBjYW5ub3QgYmUgZW1wdHksIHN0YXJ0IG9yIGVuZCB3aXRoIC8sIG9yIGNvbnRhaW4gLy8sIGJhY2tzbGFzaGVzLCBvciBudWxsIGNoYXJhY3RlcnNgKTtcbiAgICB9XG4gICAgaWYgKHBhdGguc3BsaXQoXCIvXCIpLnNvbWUocGFydCA9PiBwYXJ0ID09PSBcIi5cIiB8fCBwYXJ0ID09PSBcIi4uXCIpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBwYXRoICR7SlNPTi5zdHJpbmdpZnkocGF0aC5zbGljZSgwLCAyMDApKX0sIHBhdGhzIGNhbm5vdCBjb250YWluIC4gb3IgLi4gc2VnbWVudHNgKTtcbiAgICB9XG59XG5cbi8qKiBNZXRob2QgZGVjb3JhdG9yOiB2YWxpZGF0ZXMgdGhlIHdlbGwta25vd24gZmllbGRzIG9mIHRoZSBtZXRob2QncyBzaW5nbGUgY29uZmlnLW9iamVjdCBhcmd1bWVudCAtIGFjY291bnQvYnVja2V0TmFtZSBhcyBuYW1lcywgcGF0aCBhcyBhIHBhdGggLSBiZWZvcmUgdGhlIG1ldGhvZCBydW5zLiBGaWVsZHMgdGhlIGNvbmZpZyBkb2Vzbid0IGhhdmUgYXJlIHNraXBwZWQsIHNvIGl0IGFwcGxpZXMgdG8gZXZlcnkgQVBJIG1ldGhvZCB1bmlmb3JtbHkuIHByZWZpeCBpcyBkZWxpYmVyYXRlbHkgTk9UIHZhbGlkYXRlZDogcHJlZml4ZXMgbWF5IGJlIGVtcHR5IG9yIGVuZCB3aXRoIFwiL1wiLCBib3RoIGludmFsaWQgZm9yIHBhdGhzLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydFZhbGlkQXJncyh0YXJnZXQ6IHVua25vd24sIGtleTogc3RyaW5nLCBkZXNjcmlwdG9yOiBQcm9wZXJ0eURlc2NyaXB0b3IpOiB2b2lkIHtcbiAgICBsZXQgb3JpZ2luYWwgPSBkZXNjcmlwdG9yLnZhbHVlIGFzICguLi5hcmdzOiB1bmtub3duW10pID0+IHVua25vd247XG4gICAgZGVzY3JpcHRvci52YWx1ZSA9IGZ1bmN0aW9uICguLi5hcmdzOiB1bmtub3duW10pOiB1bmtub3duIHtcbiAgICAgICAgbGV0IGNvbmZpZyA9IGFyZ3NbMF0gYXMgeyBhY2NvdW50Pzogc3RyaW5nOyBidWNrZXROYW1lPzogc3RyaW5nOyBwYXRoPzogc3RyaW5nIH0gfCB1bmRlZmluZWQ7XG4gICAgICAgIGlmIChjb25maWcgJiYgdHlwZW9mIGNvbmZpZyA9PT0gXCJvYmplY3RcIikge1xuICAgICAgICAgICAgaWYgKFwiYWNjb3VudFwiIGluIGNvbmZpZykgYXNzZXJ0VmFsaWROYW1lKGNvbmZpZy5hY2NvdW50IGFzIHN0cmluZywgXCJhY2NvdW50XCIpO1xuICAgICAgICAgICAgaWYgKFwiYnVja2V0TmFtZVwiIGluIGNvbmZpZykgYXNzZXJ0VmFsaWROYW1lKGNvbmZpZy5idWNrZXROYW1lIGFzIHN0cmluZywgXCJidWNrZXQgbmFtZVwiKTtcbiAgICAgICAgICAgIGlmIChcInBhdGhcIiBpbiBjb25maWcpIGFzc2VydFZhbGlkUGF0aChjb25maWcucGF0aCBhcyBzdHJpbmcpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBvcmlnaW5hbC5hcHBseSh0aGlzLCBhcmdzKTtcbiAgICB9O1xufVxuIl19
50
- /* _JS_SOURCE_HASH = "3c47af61d817d76651ac4b91cc2d83a30a45619b8072517ce152b34b6075b1d8"; */
50
+ /* _JS_SOURCE_HASH = "52f73d0ef7e2f65848acc68373c50e34bd39c017c3ff908220fc66a6f3d0296f"; */
@@ -2,7 +2,7 @@
2
2
  /// <reference types="node" />
3
3
  import { LogFileInfo } from "../StreamingLogs";
4
4
  export declare const LOGS_FOLDER_NAME = "logs";
5
- /** One mutation the server performed: set/del/move/undelete/setLarge/routingConfig, plus the per-file synchronization writes ("sync get"/"sync set"). Sizes and times, never the data. internal marks writes pushed by a peer's synchronization rather than a client. */
5
+ /** One mutation the server performed: set/del/move/undelete/setLarge/routingConfig, plus the per-file synchronization writes ("sync get"/"sync set"). Sizes and times, never the data. internal marks writes pushed by a peer's synchronization rather than a client. Also printed to the console. */
6
6
  export declare function logMutation(entry: {
7
7
  op: string;
8
8
  account: string;
@@ -15,13 +15,14 @@ export declare function logMutation(entry: {
15
15
  callerId?: string;
16
16
  internal?: boolean;
17
17
  }): void;
18
- /** A synchronization key point: scans and full syncs starting/finishing, reconciles, boundary scans - what an operator greps for to see whether the fleet is converging. */
18
+ /** A synchronization key point: scans and full syncs starting/finishing, reconciles, boundary scans - what an operator greps for to see whether the fleet is converging. Also printed to the console. */
19
19
  export declare function logSyncEvent(entry: {
20
20
  event: string;
21
21
  store: string;
22
22
  source?: string;
23
23
  [key: string]: unknown;
24
24
  }): void;
25
+ /** One console.error is all an error takes (console.error and console.warn are HOOKED to feed the stream) - this just guarantees the hook is installed first, for very-early callers. */
25
26
  export declare function logStorageError(message: string): void;
26
27
  /** The log files this server holds - see StreamingLogs.listFiles. Empty on processes with no storage folder. */
27
28
  export declare function listStorageLogFiles(): Promise<LogFileInfo[]>;
@@ -9,22 +9,20 @@ import { getOwnThreadId } from "../../misc/https/certs";
9
9
 
10
10
  export const LOGS_FOLDER_NAME = "logs";
11
11
 
12
- const getLogs = lazy((): StreamingLogs | undefined => {
12
+ // NOT lazy: a failure to resolve the folder (the server config may simply not be SET yet this early in startup) must be retried on the next log call, not cached as "no logs ever"
13
+ let logsInstance: StreamingLogs | undefined;
14
+ function getLogs(): StreamingLogs | undefined {
15
+ if (logsInstance) return logsInstance;
13
16
  let folder: string;
14
17
  try {
15
18
  folder = path.join(getStorageFolder(), LOGS_FOLDER_NAME);
16
19
  } catch {
17
20
  return undefined;
18
21
  }
19
- let threadId: string | undefined;
20
- try {
21
- threadId = getOwnThreadId(getStorageServerConfigOptional()?.rootDomain || "");
22
- } catch {
23
- threadId = undefined;
24
- }
25
22
  hookErrorLogging();
26
- return new StreamingLogs({ folder, threadId });
27
- });
23
+ logsInstance = new StreamingLogs({ folder, threadId: getThreadIdSafe() });
24
+ return logsInstance;
25
+ }
28
26
 
29
27
  function firstExternalIPv4(): string | undefined {
30
28
  for (let addresses of Object.values(os.networkInterfaces())) {
@@ -35,48 +33,71 @@ function firstExternalIPv4(): string | undefined {
35
33
  return undefined;
36
34
  }
37
35
 
38
- // Computed once: none of it changes over a process's lifetime
39
- const baseFields = lazy(() => ({
40
- pid: process.pid,
41
- entryPoint: process.argv[1],
42
- ip: firstExternalIPv4(),
43
- domain: getStorageServerConfigOptional()?.domain,
44
- }));
36
+ // Memoized on SUCCESS only: the thread cert may not be loaded yet when the first entries are written, and that must not permanently strip the thread id from every later entry
37
+ let cachedThreadId: string | undefined;
38
+ function getThreadIdSafe(): string | undefined {
39
+ if (cachedThreadId) return cachedThreadId;
40
+ try {
41
+ cachedThreadId = getOwnThreadId(getStorageServerConfigOptional()?.rootDomain || "");
42
+ } catch { }
43
+ return cachedThreadId;
44
+ }
45
+
46
+ const cachedIp = lazy(() => firstExternalIPv4());
45
47
 
46
- function write(kind: string, entry: { [key: string]: unknown }): void {
48
+ // threadId and domain are re-resolved until known (early entries may predate the config/certs); the rest never changes
49
+ function baseFields() {
50
+ return {
51
+ pid: process.pid,
52
+ threadId: getThreadIdSafe(),
53
+ entryPoint: process.argv[1],
54
+ ip: cachedIp(),
55
+ domain: getStorageServerConfigOptional()?.domain,
56
+ };
57
+ }
58
+
59
+ // Everything logged here goes BOTH to the storage log stream and the console - one call, never two. The console line carries only the entry (the base fields are constant per process, so they would just be noise there); the stream gets everything.
60
+ function write(kind: string, entry: { [key: string]: unknown }, alsoConsole: boolean): void {
61
+ if (alsoConsole) {
62
+ console.log(`[${kind}] ${JSON.stringify(entry)}`);
63
+ }
47
64
  let logs = getLogs();
48
65
  if (!logs) return;
49
66
  logs.log({ kind, time: Date.now(), ...baseFields(), ...entry });
50
67
  }
51
68
 
52
- /** One mutation the server performed: set/del/move/undelete/setLarge/routingConfig, plus the per-file synchronization writes ("sync get"/"sync set"). Sizes and times, never the data. internal marks writes pushed by a peer's synchronization rather than a client. */
69
+ /** One mutation the server performed: set/del/move/undelete/setLarge/routingConfig, plus the per-file synchronization writes ("sync get"/"sync set"). Sizes and times, never the data. internal marks writes pushed by a peer's synchronization rather than a client. Also printed to the console. */
53
70
  export function logMutation(entry: { op: string; account: string; bucketName: string; store?: string; path: string; toPath?: string; size?: number; writeTime?: number; callerId?: string; internal?: boolean }): void {
54
- write("mutation", entry);
71
+ write("mutation", entry, true);
55
72
  }
56
73
 
57
- /** A synchronization key point: scans and full syncs starting/finishing, reconciles, boundary scans - what an operator greps for to see whether the fleet is converging. */
74
+ /** A synchronization key point: scans and full syncs starting/finishing, reconciles, boundary scans - what an operator greps for to see whether the fleet is converging. Also printed to the console. */
58
75
  export function logSyncEvent(entry: { event: string; store: string; source?: string; [key: string]: unknown }): void {
59
- write("sync", entry);
76
+ write("sync", entry, true);
60
77
  }
61
78
 
79
+ /** One console.error is all an error takes (console.error and console.warn are HOOKED to feed the stream) - this just guarantees the hook is installed first, for very-early callers. */
62
80
  export function logStorageError(message: string): void {
63
- write("error", { message });
81
+ getLogs();
82
+ console.error(message);
64
83
  }
65
84
 
66
- // Everything console.errored also lands in the log stream (with a reentrancy guard: a failure INSIDE logging must not log itself forever)
67
- let writingError = false;
85
+ // Everything console.errored or console.warned also lands in the log stream (with a reentrancy guard: a failure INSIDE logging must not log itself forever)
86
+ let writingHooked = false;
68
87
  const hookErrorLogging = lazy(() => {
69
- let original = console.error.bind(console);
70
- console.error = (...args: unknown[]) => {
71
- original(...args);
72
- if (writingError) return;
73
- writingError = true;
74
- try {
75
- logStorageError(args.map(x => typeof x === "string" && x || ((x as Error)?.stack ?? JSON.stringify(x))).join(" ").slice(0, 5000));
76
- } catch { } finally {
77
- writingError = false;
78
- }
79
- };
88
+ for (let level of ["error", "warn"] as const) {
89
+ let original = console[level].bind(console);
90
+ console[level] = (...args: unknown[]) => {
91
+ original(...args);
92
+ if (writingHooked) return;
93
+ writingHooked = true;
94
+ try {
95
+ write(level, { message: args.map(x => typeof x === "string" && x || ((x as Error)?.stack ?? JSON.stringify(x))).join(" ").slice(0, 5000) }, false);
96
+ } catch { } finally {
97
+ writingHooked = false;
98
+ }
99
+ };
100
+ }
80
101
  });
81
102
 
82
103
  /** The log files this server holds - see StreamingLogs.listFiles. Empty on processes with no storage folder. */
@@ -37,14 +37,16 @@ export declare class StoreSync {
37
37
  waitForRequiredScans(): Promise<void>;
38
38
  /** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
39
39
  rescanBase(): Promise<void>;
40
+ /** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. */
41
+ private syncSource;
40
42
  /** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
41
43
  boundaryScanRemote(source: IArchives, config: {
42
44
  since: number;
43
45
  route?: [number, number];
44
46
  }): Promise<void>;
45
47
  private runSourceSync;
46
- private scanSource;
47
- private reconcileSource;
48
+ private pullSource;
49
+ private pushSource;
48
50
  private updateScanIndex;
49
51
  private pollChanges;
50
52
  private copySourceFiles;
@@ -48,6 +48,8 @@ type SourceState = {
48
48
  supportsChangesAfter: boolean;
49
49
  initialScan: ReturnType<typeof promiseObj>;
50
50
  scanComplete: boolean;
51
+ // Whether a scan of this source has ever actually SUCCEEDED - what full syncs gate on: without one, the index's view of this source can be arbitrarily stale (e.g. it was restarting when we first tried), and syncing from stale information churns forever
52
+ scanSucceeded: boolean;
51
53
  // Files seen in this source's scans / change polls so far
52
54
  scannedCount: number;
53
55
  // Watermark for getChangesAfter2 polls
@@ -63,6 +65,7 @@ function newSourceState(): SourceState {
63
65
  supportsChangesAfter: false,
64
66
  initialScan: promiseObj(),
65
67
  scanComplete: false,
68
+ scanSucceeded: false,
66
69
  scannedCount: 0,
67
70
  changesAfterTime: 0,
68
71
  stopped: { stop: false },
@@ -75,10 +78,6 @@ type ScanTally = Record<ScanOutcome, number>;
75
78
  function newScanTally(): ScanTally {
76
79
  return { filtered: 0, new: 0, updated: 0, tombstone: 0, unchanged: 0 };
77
80
  }
78
- function formatScanTally(tally: ScanTally, total: number): string {
79
- let pct = (n: number) => `${Math.round(n / Math.max(total, 1) * 1000) / 10}%`;
80
- return `${tally.new} new paths (${pct(tally.new)}), ${tally.updated} newer writes (${pct(tally.updated)}), ${tally.tombstone} deletions (${pct(tally.tombstone)}), ${tally.unchanged} unchanged (${pct(tally.unchanged)}), ${tally.filtered} outside route (${pct(tally.filtered)})`;
81
- }
82
81
 
83
82
  export class StoreSync {
84
83
  // Per source slot, in step with the store's sources array. Slots are never spliced out - a removed one is marked dead - so a slot number held by a running loop always means the same source.
@@ -220,13 +219,24 @@ export class StoreSync {
220
219
 
221
220
  /** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
222
221
  public async rescanBase(): Promise<void> {
223
- await this.scanSource(0);
222
+ await this.syncSource(0);
223
+ }
224
+
225
+ /** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. */
226
+ private async syncSource(sourceIndex: number, push?: "push"): Promise<void> {
227
+ let listing = await this.pullSource(sourceIndex);
228
+ let state = this.states[sourceIndex];
229
+ state.scanComplete = true;
230
+ state.initialScan.resolve(undefined);
231
+ if (push && !this.store.stopped.stop && !state.stopped.stop) {
232
+ await this.pushSource(sourceIndex, listing);
233
+ }
224
234
  }
225
235
 
226
236
  /** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
227
237
  public async boundaryScanRemote(source: IArchives, config: { since: number; route?: [number, number] }): Promise<void> {
228
238
  let scanStart = Date.now();
229
- console.log(`Boundary scan of ${source.getDebugName()} starting: changes since ${new Date(config.since).toISOString()}, route ${JSON.stringify(config.route || FULL_ROUTE)} (store ${this.store.folder})`);
239
+ logSyncEvent({ event: "boundaryScanStart", store: this.store.folder, source: source.getDebugName(), since: config.since, route: config.route || FULL_ROUTE });
230
240
  let changes = await source.getChangesAfter2({ time: config.since, routes: config.route && [config.route] || undefined });
231
241
  let tally = newScanTally();
232
242
  for (let file of changes) {
@@ -246,7 +256,10 @@ export class StoreSync {
246
256
  continue;
247
257
  }
248
258
  let copied = await copyArchiveFile({ from: source, to: this.store.sources[0].source, path: file.path, size: file.size, writeTime: file.createTime, forceSetImmutable: true, noChecks: true, internal: true });
249
- if (!copied) continue;
259
+ if (!copied) {
260
+ console.warn(`Boundary scan could not copy ${file.path} from ${source.getDebugName()} (store ${this.store.folder}): its change feed listed it (${file.size} bytes, writeTime ${new Date(file.createTime).toISOString()}) but the read found nothing`);
261
+ continue;
262
+ }
250
263
  this.store.noteSyncTransfer("sync get", file.path, copied.size);
251
264
  if (copied.size === 0) {
252
265
  this.store.setIndexDeleted(file.path, copied.writeTime);
@@ -260,8 +273,7 @@ export class StoreSync {
260
273
  tally.new++;
261
274
  }
262
275
  }
263
- console.log(`Boundary scan of ${source.getDebugName()} finished in ${Math.round((Date.now() - scanStart) / 1000)}s (store ${this.store.folder}): ${changes.length} changes: ${formatScanTally(tally, changes.length)}`);
264
- logSyncEvent({ event: "boundaryScanFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - scanStart, since: config.since, changes: changes.length, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone });
276
+ logSyncEvent({ event: "boundaryScanFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - scanStart, since: config.since, changes: changes.length, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone, unchanged: tally.unchanged });
265
277
  }
266
278
 
267
279
  // ── per-source loops ──
@@ -270,9 +282,10 @@ export class StoreSync {
270
282
  await this.store.registerSlot(sourceIndex);
271
283
  let { source } = this.store.sources[sourceIndex];
272
284
  let state = this.states[sourceIndex];
285
+ // If a slot ever gets TWO of these, its loops are doubled - every poll and full sync runs twice
286
+ logSyncEvent({ event: "sourceSyncStart", store: this.store.folder, source: source.getDebugName(), sourceIndex, url: this.store.sources[sourceIndex].url, intermediate: this.store.sources[sourceIndex].intermediate });
273
287
  // Read live for every pass, not captured - the store's source list can change while loops run
274
288
  let noFullSync = () => this.store.sources[sourceIndex].noFullSync;
275
- let listing: Map<string, number> | undefined;
276
289
  // An intermediate is a deploy switchover's temporary alternate PORT onto a source we already have: the same bucket, reachable another way for a few minutes. Scanning it would list exactly what scanning that source lists, against a port that is about to disappear - so it is never scanned, and the source it was split out of covers it for as long as it exists and after it is gone.
277
290
  if (this.store.sources[sourceIndex].intermediate) {
278
291
  state.scanComplete = true;
@@ -283,19 +296,17 @@ export class StoreSync {
283
296
  try {
284
297
  let config = await source.getConfig();
285
298
  state.supportsChangesAfter = !!config.supportsChangesAfter;
286
- listing = await this.scanSource(sourceIndex);
299
+ await this.syncSource(sourceIndex, "push");
287
300
  break;
288
301
  } catch (e) {
289
- console.error(`Initial scan of sync source ${source.getDebugName()} failed, retrying:`, e);
302
+ logSyncEvent({ event: "initialScanFailed", store: this.store.folder, source: source.getDebugName(), retryInMs: SCAN_RETRY_DELAY, error: String((e as Error).stack ?? e).slice(0, 2000) });
290
303
  await delay(SCAN_RETRY_DELAY);
291
304
  }
292
305
  }
306
+ // The loop can also exit by the stop tokens, and nothing may wait forever on a stopped source's initial scan
293
307
  state.scanComplete = true;
294
308
  state.initialScan.resolve(undefined);
295
309
  if (this.store.stopped.stop || state.stopped.stop) return;
296
- if (listing) {
297
- await this.reconcileSource(sourceIndex, listing);
298
- }
299
310
  if (!noFullSync()) {
300
311
  try {
301
312
  await this.copySourceFiles(sourceIndex);
@@ -305,31 +316,27 @@ export class StoreSync {
305
316
  }
306
317
  if (state.supportsChangesAfter) {
307
318
  runInfinitePoll(CHANGES_POLL_INTERVAL, async () => {
319
+ // A scan that has not succeeded yet is retried HERE, before anything else - polling changes and copying against a never-scanned source would run on arbitrarily stale information
320
+ if (!state.scanSucceeded) {
321
+ await this.syncSource(sourceIndex, "push");
322
+ }
308
323
  await this.pollChanges(sourceIndex);
309
324
  if (!noFullSync()) await this.copySourceFiles(sourceIndex);
310
325
  }, state.stopped);
311
- // Change polls only show what the source HAS, never what it's missing, so pushes run on the full-rescan cadence (findInfo on an index-backed source is cheap)
312
- runInfinitePoll(FULL_RESCAN_INTERVAL, async () => {
313
- let files = await source.findInfo("");
314
- await this.reconcileSource(sourceIndex, new Map(files.map(x => [x.path, x.createTime])));
315
- }, state.stopped);
316
- } else {
317
- runInfinitePoll(FULL_RESCAN_UNINDEXED_INTERVAL, async () => {
318
- let rescan = await this.scanSource(sourceIndex);
319
- await this.reconcileSource(sourceIndex, rescan);
320
- if (!noFullSync()) await this.copySourceFiles(sourceIndex);
321
- }, state.stopped);
322
326
  }
327
+ runInfinitePoll(FULL_RESCAN_UNINDEXED_INTERVAL, async () => {
328
+ await this.syncSource(sourceIndex, "push");
329
+ if (!noFullSync()) await this.copySourceFiles(sourceIndex);
330
+ }, state.stopped);
323
331
  }
324
332
 
325
- // Full metadata scan (size, writeTime, path) of one source, applied to the index. Returns the source's listing (path -> write time), which reconcileSource uses for the push direction.
326
- private async scanSource(sourceIndex: number): Promise<Map<string, number>> {
333
+ // The PULL direction: a full metadata scan (size, writeTime, path) of one source, applied to our index. Returns the source's listing (path -> write time), which pushSource uses for the opposite direction.
334
+ private async pullSource(sourceIndex: number): Promise<Map<string, number>> {
327
335
  let { source, route } = this.store.sources[sourceIndex];
328
336
  let state = this.states[sourceIndex];
329
337
  let scanStart = Date.now();
330
338
  let activity: SyncActivity = { type: "metadataScan", sourceDebugName: source.getDebugName(), startTime: scanStart };
331
339
  this.activities.add(activity);
332
- console.log(`Metadata scan of ${source.getDebugName()} starting (store ${this.store.folder})`);
333
340
  logSyncEvent({ event: "scanStart", store: this.store.folder, source: source.getDebugName() });
334
341
  let progressTimer = setInterval(() => {
335
342
  console.log(`Metadata scan of ${source.getDebugName()} still running (${Math.round((Date.now() - scanStart) / 1000)}s, store ${this.store.folder})`);
@@ -392,17 +399,14 @@ export class StoreSync {
392
399
  if (!routeContains(route, getRoute(key))) continue;
393
400
  missingOnSource++;
394
401
  }
395
- // Percentages are of the union of both sides (our index + their listing), so every count has a stable denominator
396
- let union = indexSizeBefore + newPaths;
397
- let pct = (n: number) => `${Math.round(n / Math.max(union, 1) * 1000) / 10}%`;
398
- console.log(`Metadata scan of ${source.getDebugName()} finished in ${Math.round((Date.now() - scanStart) / 1000)}s (store ${this.store.folder}): ${files.length} listed vs ${indexSizeBefore} indexed (union ${union}): ${formatScanTally(tally, union)}, ${missingOnSource} in index but missing on source (${pct(missingOnSource)}), ${removedFromIndex} removed from index (${pct(removedFromIndex)})`);
399
- logSyncEvent({ event: "scanFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - scanStart, listed: files.length, indexedBefore: indexSizeBefore, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone, missingOnSource, removedFromIndex });
402
+ logSyncEvent({ event: "scanFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - scanStart, listed: files.length, indexedBefore: indexSizeBefore, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone, unchanged: tally.unchanged, outsideRoute: tally.filtered, missingOnSource, removedFromIndex });
400
403
  state.changesAfterTime = Math.max(state.changesAfterTime, scanStart - CHANGES_POLL_OVERLAP);
404
+ state.scanSucceeded = true;
401
405
  return seen;
402
406
  }
403
407
 
404
408
  // The push direction of synchronization: everything we know that the source is missing (or holds an older copy of) is written to it — including deletions, as tombstone writes. This is what heals a source whose background writes failed (e.g. it was down): the next scan sees what's missing and re-sends it. A failing file is skipped, not fatal (immutable targets are handled by forceSetImmutable, and one unreadable value must not stop the rest of the pass) - only a run of consecutive failures (the source itself is down) aborts until the next scan cycle.
405
- private async reconcileSource(sourceIndex: number, listing: Map<string, number>): Promise<void> {
409
+ private async pushSource(sourceIndex: number, listing: Map<string, number>): Promise<void> {
406
410
  let { source, validWindows, route } = this.store.sources[sourceIndex];
407
411
  let state = this.states[sourceIndex];
408
412
  let acceptsWrites = windowsAcceptWrites(validWindows);
@@ -462,12 +466,8 @@ export class StoreSync {
462
466
  }
463
467
  if (failed) {
464
468
  console.error(`Reconciling sync source ${source.getDebugName()} (store ${this.store.folder}): pushed ${pushed} files, ${failed} failed${aborted && ` before aborting the pass (${consecutiveFailures} consecutive failures - the source looks down; the next scan cycle retries)` || ""}. First errors: ${errors.join(" | ")}`);
465
- } else if (pushed) {
466
- console.log(`Reconciled sync source ${source.getDebugName()} (store ${this.store.folder}): pushed ${pushed} files it was missing or held older copies of`);
467
- }
468
- if (pushed || failed) {
469
- logSyncEvent({ event: "reconcileFinish", store: this.store.folder, source: source.getDebugName(), pushed, failed, aborted });
470
469
  }
470
+ logSyncEvent({ event: "reconcileFinish", store: this.store.folder, source: source.getDebugName(), pushed, failed, aborted });
471
471
  }
472
472
 
473
473
  private updateScanIndex(sourceIndex: number, file: ArchiveFileInfo): ScanOutcome {
@@ -503,9 +503,9 @@ export class StoreSync {
503
503
  for (let file of changes) {
504
504
  tally[this.updateScanIndex(sourceIndex, file)]++;
505
505
  }
506
- // Polls run constantly, so only the ones that actually changed the index get a line
506
+ // Polls run constantly, so only the ones that actually changed the index get an entry
507
507
  if (tally.new || tally.updated || tally.tombstone) {
508
- console.log(`Changes poll of ${source.getDebugName()} (store ${this.store.folder}): ${changes.length} changes: ${formatScanTally(tally, changes.length)}`);
508
+ logSyncEvent({ event: "changesPoll", store: this.store.folder, source: source.getDebugName(), changes: changes.length, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone, unchanged: tally.unchanged });
509
509
  }
510
510
  state.scannedCount += changes.length;
511
511
  state.changesAfterTime = pollStart - CHANGES_POLL_OVERLAP;
@@ -535,7 +535,7 @@ export class StoreSync {
535
535
  totalBytes,
536
536
  };
537
537
  this.activities.add(activity);
538
- logSyncEvent({ event: "fullSyncStart", store: this.store.folder, source: source.getDebugName(), files: pending.length, bytes: totalBytes });
538
+ logSyncEvent({ event: "fullSyncStart", store: this.store.folder, source: source.getDebugName(), diffBased: state.supportsChangesAfter, files: pending.length, bytes: totalBytes });
539
539
  let progressLogged = false;
540
540
  let logProgress = () => {
541
541
  progressLogged = true;
@@ -557,12 +557,14 @@ export class StoreSync {
557
557
  console.warn(`Full sync from ${source.getDebugName()} (store ${this.store.folder}) has been running for ${formatTime(elapsed)}: ${doneFiles}/${pending.length} files (${(doneFiles / pending.length * 100).toFixed(1)}%), ${formatNumber(doneBytes)}B/${formatNumber(totalBytes)}B (${(totalBytes && doneBytes / totalBytes * 100 || 100).toFixed(1)}%), ${formatNumber(bytesPerSecond)}B/s. Estimated ${etaText}.`);
558
558
  }, FULL_SYNC_SLOW_ERROR_INTERVAL);
559
559
  (slowErrorTimer as { unref?: () => void }).unref?.();
560
+ let failed = 0;
561
+ let copiedFiles = 0;
562
+ let copiedBytes = 0;
563
+ let missingOnSource = 0;
564
+ let aborted = false;
560
565
  try {
561
566
  let nextIndex = 0;
562
- let failed = 0;
563
567
  let consecutiveFailures = 0;
564
- let errors: string[] = [];
565
- let aborted = false;
566
568
  let copyWorker = async () => {
567
569
  while (!aborted && !this.store.stopped.stop && !state.stopped.stop) {
568
570
  let index = nextIndex++;
@@ -571,10 +573,26 @@ export class StoreSync {
571
573
  try {
572
574
  let copied = await copyArchiveFile({ from: source, to: this.store.sources[0].source, path: key, size: entry.size, writeTime: entry.writeTime, forceSetImmutable: true, noChecks: true, internal: true });
573
575
  if (copied) {
576
+ copiedFiles++;
577
+ copiedBytes += copied.size;
574
578
  this.store.noteSyncTransfer("sync get", key, copied.size);
575
- // Only move the entry's source if it wasn't changed while we copied
576
- if (this.entryUnchanged(key, entry)) {
577
- this.store.setIndexEntry(key, { writeTime: copied.writeTime, size: copied.size, sourcesListIndex: this.store.sourcesListIndexOfSlot(0) });
579
+ // The copy carries the source's write time, and the index commits it under the normal ordering rule (>= the current time wins) - it refusing means a NEWER write landed while we copied, which must be said, not swallowed
580
+ if (!this.store.setIndexEntry(key, { writeTime: copied.writeTime, size: copied.size, sourcesListIndex: this.store.sourcesListIndexOfSlot(0) })) {
581
+ logSyncEvent({ event: "fullSyncCommitSuperseded", store: this.store.folder, source: source.getDebugName(), path: key, copiedWriteTime: copied.writeTime, currentWriteTime: this.store.currentWriteTime(key) });
582
+ }
583
+ } else {
584
+ missingOnSource++;
585
+ // The source's listing vouched for the file but its read has nothing - and asking again forever is not an answer. If our own disk holds a copy, IT is the real holder, so the entry repoints there; if neither side has bytes, the entry was simply wrong, and it is purged - forgotten, not deleted, exactly like a read that comes up empty everywhere (a scan re-finds the file if it exists anywhere else).
586
+ let local = await this.store.sources[0].source.getInfo(key);
587
+ if (local) {
588
+ this.store.setIndexEntry(key, { writeTime: entry.writeTime, size: local.size, sourcesListIndex: this.store.sourcesListIndexOfSlot(0) });
589
+ logSyncEvent({ event: "fullSyncMissingOnSource", store: this.store.folder, source: source.getDebugName(), path: key, expectedSize: entry.size, expectedWriteTime: entry.writeTime, resolution: "repointed to our local copy", localSize: local.size });
590
+ } else if (this.store.currentWriteTime(key) <= entry.writeTime) {
591
+ this.store.purgeIndexEntry(key);
592
+ logSyncEvent({ event: "fullSyncMissingOnSource", store: this.store.folder, source: source.getDebugName(), path: key, expectedSize: entry.size, expectedWriteTime: entry.writeTime, resolution: "purged - neither the source nor our disk has it, so it does not exist" });
593
+ } else {
594
+ // A newer write landed while we were checking - the entry is no longer the one we found missing, so it is left alone
595
+ logSyncEvent({ event: "fullSyncMissingOnSource", store: this.store.folder, source: source.getDebugName(), path: key, expectedSize: entry.size, expectedWriteTime: entry.writeTime, resolution: "kept - a newer write appeared while checking", currentWriteTime: this.store.currentWriteTime(key) });
578
596
  }
579
597
  }
580
598
  consecutiveFailures = 0;
@@ -582,9 +600,7 @@ export class StoreSync {
582
600
  // One failing file must not abort the sync: it stays on its source (reads still reach it there), and the next sync pass retries it
583
601
  failed++;
584
602
  consecutiveFailures++;
585
- if (errors.length < SYNC_ERROR_LOG_LIMIT) {
586
- errors.push(`${key}: ${(e as Error).stack ?? e}`);
587
- }
603
+ logSyncEvent({ event: "fullSyncCopyFailed", store: this.store.folder, source: source.getDebugName(), path: key, size: entry.size, error: String((e as Error).stack ?? e).slice(0, 2000) });
588
604
  if (consecutiveFailures >= SYNC_MAX_CONSECUTIVE_FAILURES) {
589
605
  aborted = true;
590
606
  return;
@@ -601,9 +617,6 @@ export class StoreSync {
601
617
  workers.push(copyWorker());
602
618
  }
603
619
  await Promise.all(workers);
604
- if (failed) {
605
- console.error(`Full sync from ${source.getDebugName()} (store ${this.store.folder}): ${failed} of ${pending.length} files failed to copy${aborted && ` before aborting the pass (${SYNC_MAX_CONSECUTIVE_FAILURES} consecutive failures - the source looks down; the next sync pass retries)` || " (they stay on their source, and the next sync pass retries them)"}. First errors: ${errors.join(" | ")}`);
606
- }
607
620
  } finally {
608
621
  clearInterval(progressTimer);
609
622
  clearInterval(slowErrorTimer);
@@ -612,7 +625,7 @@ export class StoreSync {
612
625
  if (progressLogged) {
613
626
  logProgress();
614
627
  }
615
- logSyncEvent({ event: "fullSyncFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - activity.startTime, copiedFiles: activity.doneFiles, copiedBytes: activity.doneBytes, totalFiles: pending.length, totalBytes });
628
+ logSyncEvent({ event: "fullSyncFinish", store: this.store.folder, source: source.getDebugName(), diffBased: state.supportsChangesAfter, durationMs: Date.now() - activity.startTime, totalFiles: pending.length, totalBytes, processedFiles: activity.doneFiles, copiedFiles, copiedBytes, missingOnSource, failed, aborted });
616
629
  }
617
630
  }
618
631